Merge "Disable flaky app launch from lockscree assertion" into main
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index 47e1dad..1ce8f9f 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -1,87 +1,96 @@
 # Usage
-##  Two options to use the uinput command:
-### 1. Interactive through stdin:
-type `uinput -` into the terminal, then type/paste commands to send to the binary.
-Use Ctrl+D to signal end of stream to the binary (EOF).
 
-This mode can be also used from an app to send uinput events.
-For an example, see the cts test case at: [InputTestCase.java][2]
+There are two ways to use the `uinput` command:
 
-When using another program to control uinput in interactive mode, registering a
-new input device (for example, a bluetooth joystick) should be the first step.
-After the device is added, you need to wait for the _onInputDeviceAdded_
-(see [InputDeviceListener][1]) notification before issuing commands
-to the device.
-Failure to do so will cause missed events and inconsistent behavior.
+* **Recommended:** `uinput -` reads commands from standard input until End-of-File (Ctrl+D) is sent.
+  This mode can be used interactively from a terminal or used to control uinput from another program
+  or app (such as the CTS tests via [`UinputDevice`][UinputDevice]).
+* `uinput <filename>` reads commands from a file instead of standard input.
 
-### 2. Using a file as an input:
-type `uinput <filename>`, and the file will be used an an input to the binary.
-You must add a sufficient delay after a "register" command to ensure device
-is ready. The interactive mode is the recommended method of communicating
-with the uinput binary.
+[UinputDevice]: https://cs.android.com/android/platform/superproject/main/+/main:cts/libs/input/src/com/android/cts/input/UinputDevice.java
 
-All of the input commands should be in pseudo-JSON format as documented below.
-See examples [here][3].
+## Command format
 
-The file can have multiple commands one after the other (which is not strictly
-legal JSON format, as this would imply multiple root elements).
+Input commands should be in JSON format, though the parser is in [lenient mode] to allow comments,
+and integers can be specified in hexadecimal (e.g. `0xABCD`). The input file (or standard input) can
+contain multiple commands, which will be executed in sequence. Simply add multiple JSON objects to
+the file, one after the other without separators:
 
-## Command description
+```json5
+{
+  "id": 1,
+  "command": "register",
+  // ...
+}
+{
+  "id": 1,
+  "command": "delay",
+  // ...
+}
+```
 
-1. `register`
+Many examples of command files can be found [in the CTS tests][cts-example-jsons].
+
+[lenient mode]: https://developer.android.com/reference/android/util/JsonReader#setLenient(boolean)
+[cts-example-jsons]: https://cs.android.com/android/platform/superproject/main/+/main:cts/tests/tests/hardware/res/raw/
+
+## Command reference
+
+### `register`
+
 Register a new uinput device
 
-| Field         | Type          | Description                |
+| Field            | Type           | Description                |
+|:----------------:|:--------------:|:-------------------------- |
+| `id`             | integer        | Device ID                  |
+| `command`        | string         | Must be set to "register"  |
+| `name`           | string         | Device name                |
+| `vid`            | 16-bit integer | Vendor ID                  |
+| `pid`            | 16-bit integer | Product ID                 |
+| `bus`            | string         | Bus that device should use |
+| `configuration`  | object array   | uinput device configuration|
+| `ff_effects_max` | integer        | `ff_effects_max` value     |
+| `abs_info`       | array          | Absolute axes information  |
+
+`id` is used for matching the subsequent commands to a specific device to avoid ambiguity when
+multiple devices are registered.
+
+`bus` is used to determine how the uinput device is connected to the host. The options are `"usb"`
+and `"bluetooth"`.
+
+Device configuration is used to configure the uinput device. The `type` field provides a `UI_SET_*`
+control code, and data is a vector of control values to be sent to the uinput device, which depends
+on the control code.
+
+| Field         |     Type      | Description                |
 |:-------------:|:-------------:|:-------------------------- |
-| id            | integer       | Device id                  |
-| command       | string        | Must be set to "register"  |
-| name          | string        | Device name                |
-| vid           | 16-bit integer| Vendor id                  |
-| pid           | 16-bit integer| Product id                 |
-| bus           | string        | Bus that device should use |
-| configuration | int array     | uinput device configuration|
-| ff_effects_max| integer       | ff_effects_max value       |
-| abs_info      | array         | ABS axes information       |
+| `type`        | integer       | `UI_SET_` control type     |
+| `data`        | integer array | control values             |
 
-Device ID is used for matching the subsequent commands to a specific device
-to avoid ambiguity when multiple devices are registered.
+`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`.
 
-Device bus is used to determine how the uinput device is connected to the host.
-The options are "usb" and "bluetooth".
-
-Device configuration is used to configure uinput device.  "type" field provides the UI_SET_*
-control code, and data is a vector of control values to be sent to uinput device, depends on
-the control code.
+`abs_info` fields are provided to set the device axes information. It is an array of below objects:
 
 | Field         | Type          | Description                |
 |:-------------:|:-------------:|:-------------------------- |
-| type          | integer       | UI_SET_ control type       |
-| data          | int array     | control values             |
+| `code`        | integer       | Axis code                  |
+| `info`        | object        | Axis information object    |
 
-Device ff_effects_max must be provided if FFBIT is set.
+The axis information object is defined as below, with the fields having the same meaning as those
+Linux's [`struct input_absinfo`][struct input_absinfo]:
 
-Device abs_info fields are provided to set the device axes information. It is an array of below
-objects:
 | Field         | Type          | Description                |
 |:-------------:|:-------------:|:-------------------------- |
-| code          | integer       | Axis code                  |
-| info          | object        | ABS information object     |
-
-ABS information object is defined as below:
-| Field         | Type          | Description                |
-|:-------------:|:-------------:|:-------------------------- |
-| value         | integer       | Latest reported value      |
-| minimum       | integer       | Minimum value for the axis |
-| maximum       | integer       | Maximum value for the axis |
-| fuzz          | integer       | fuzz value for noise filter|
-| flat          | integer       | values to be discarded     |
-| resolution    | integer       | resolution of axis         |
-
-See [struct input_absinfo][4]) definitions.
+| `value`       | integer       | Latest reported value      |
+| `minimum`     | integer       | Minimum value for the axis |
+| `maximum`     | integer       | Maximum value for the axis |
+| `fuzz`        | integer       | fuzz value for noise filter|
+| `flat`        | integer       | values to be discarded     |
+| `resolution`  | integer       | resolution of axis         |
 
 Example:
-```json
 
+```json5
 {
   "id": 1,
   "command": "register",
@@ -90,9 +99,9 @@
   "pid": 0x2c42,
   "bus": "usb",
   "configuration":[
-        {"type":100, "data":[1, 21]},  // UI_SET_EVBIT : EV_KEY and EV_FF
+        {"type":100, "data":[1, 21]},         // UI_SET_EVBIT : EV_KEY and EV_FF
         {"type":101, "data":[11, 2, 3, 4]},   // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3
-        {"type":107, "data":[80]}    //  UI_SET_FFBIT : FF_RUMBLE
+        {"type":107, "data":[80]}             // UI_SET_FFBIT : FF_RUMBLE
   ],
   "ff_effects_max" : 1,
   "abs_info": [
@@ -104,19 +113,39 @@
         }
   ]
 }
-
 ```
-2. `delay`
+
+[struct input_absinfo]: https://cs.android.com/android/platform/superproject/main/+/main:bionic/libc/kernel/uapi/linux/input.h?q=%22struct%20input_absinfo%22
+
+#### Waiting for registration
+
+After the command is sent, there will be a delay before the device is set up by the Android input
+stack, and `uinput` does not wait for that process to finish. Any commands sent to the device during
+that time will be dropped. If you are controlling `uinput` by sending commands through standard
+input from an app, you need to wait for [`onInputDeviceAdded`][onInputDeviceAdded] to be called on
+an `InputDeviceListener` before issuing commands to the device. If you are passing a file to
+`uinput`, add a `delay` after the `register` command to let registration complete.
+
+[onInputDeviceAdded]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html
+
+#### Unregistering the device
+
+As soon as EOF is reached (either in interactive mode, or in file mode), the device that was created
+will be unregistered. There is no explicit command for unregistering a device.
+
+### `delay`
+
 Add a delay to command processing
 
 | Field         | Type          | Description                |
 |:-------------:|:-------------:|:-------------------------- |
-| id            | integer       | Device id                  |
-| command       | string        | Must be set to "delay"     |
-| duration      | integer       | Delay in milliseconds      |
+| `id`          | integer       | Device ID                  |
+| `command`     | string        | Must be set to "delay"     |
+| `duration`    | integer       | Delay in milliseconds      |
 
 Example:
-```json
+
+```json5
 {
   "id": 1,
   "command": "delay",
@@ -124,20 +153,21 @@
 }
 ```
 
-3. `inject`
-Send an array of uinput event packets [type, code, value] to the uinput device
+### `inject`
+
+Send an array of uinput event packets to the uinput device
 
 | Field         | Type          | Description                |
 |:-------------:|:-------------:|:-------------------------- |
-| id            | integer       | Device id                  |
-| command       | string        | Must be set to "inject"    |
-| events        | integer array | events to inject           |
+| `id`          | integer       | Device ID                  |
+| `command`     | string        | Must be set to "inject"    |
+| `events`      | integer array | events to inject           |
 
-The "events" parameter is an array of integers, encapsulates evdev input_event type, code and value,
-see the example below.
+The `events` parameter is an array of integers in sets of three: a type, an axis code, and an axis
+value, like you'd find in Linux's `struct input_event`. For example, sending presses of the 0 and 1
+keys would look like this:
 
-Example:
-```json
+```json5
 {
   "id": 1,
   "command": "inject",
@@ -153,14 +183,6 @@
 }
 ```
 
-### Notes
-1. As soon as EOF is reached (either in interactive mode, or in file mode),
-the device that was created will be unregistered. There is no
-explicit command for unregistering a device.
-2. The `getevent` utility can used to print out the key events
-for debugging purposes.
+## Notes
 
-[1]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html
-[2]: ../../../../cts/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java
-[3]: ../../../../cts/tests/tests/hardware/res/raw/
-[4]: ../../../../bionic/libc/kernel/uapi/linux/input.h
+The `getevent` utility can used to print out the key events for debugging purposes.
diff --git a/core/api/current.txt b/core/api/current.txt
index eaefe84..0d73695 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26391,6 +26391,17 @@
     method public abstract void onDisconnect(android.media.midi.MidiReceiver);
   }
 
+  public abstract class MidiUmpDeviceService extends android.app.Service {
+    ctor public MidiUmpDeviceService();
+    method @Nullable public final android.media.midi.MidiDeviceInfo getDeviceInfo();
+    method @NonNull public final java.util.List<android.media.midi.MidiReceiver> getOutputPortReceivers();
+    method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+    method public void onClose();
+    method public void onDeviceStatusChanged(@Nullable android.media.midi.MidiDeviceStatus);
+    method @NonNull public abstract java.util.List<android.media.midi.MidiReceiver> onGetInputPortReceivers();
+    field public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService";
+  }
+
 }
 
 package android.media.projection {
diff --git a/core/java/android/app/HomeVisibilityListener.java b/core/java/android/app/HomeVisibilityListener.java
index ca20648..0b5a5ed1 100644
--- a/core/java/android/app/HomeVisibilityListener.java
+++ b/core/java/android/app/HomeVisibilityListener.java
@@ -25,6 +25,7 @@
 import android.annotation.TestApi;
 import android.content.Context;
 import android.os.Binder;
+import android.util.Log;
 
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -40,6 +41,8 @@
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 @TestApi
 public abstract class HomeVisibilityListener {
+    private static final String TAG = HomeVisibilityListener.class.getSimpleName();
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private ActivityTaskManager mActivityTaskManager;
     private Executor mExecutor;
     private int mMaxScanTasksForHomeVisibility;
@@ -102,6 +105,11 @@
 
         for (int i = 0, taskSize = tasksTopToBottom.size(); i < taskSize; ++i) {
             ActivityManager.RunningTaskInfo task = tasksTopToBottom.get(i);
+            if (DBG) {
+                Log.d(TAG, "Task#" + i + ": activity=" + task.topActivity
+                        + ", visible=" + task.isVisible()
+                        + ", flg=" + Integer.toHexString(task.baseIntent.getFlags()));
+            }
             if (!task.isVisible()
                     || (task.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
                 continue;
diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index 729e555..0857c96 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -40,7 +40,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -195,7 +195,8 @@
         XmlUtils.beginDocument(parser, TAG_LOCALE_CONFIG);
         int outerDepth = parser.getDepth();
         AttributeSet attrs = Xml.asAttributeSet(parser);
-        Set<String> localeNames = new HashSet<String>();
+        // LinkedHashSet to preserve insertion order
+        Set<String> localeNames = new LinkedHashSet<>();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
             if (TAG_LOCALE.equals(parser.getName())) {
                 final TypedArray attributes = res.obtainAttributes(
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 0f66e93..8da8442 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -16,7 +16,12 @@
         },
         {
             "file_patterns": ["(/|^)AppOpsManager.java"],
-            "name": "CtsAppOpsTestCases"
+            "name": "CtsAppOpsTestCases",
+            "options": [
+                {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                }
+            ]
         },
         {
             "file_patterns": ["(/|^)AppOpsManager.java"],
@@ -57,7 +62,7 @@
             "name": "CtsWindowManagerDeviceTestCases",
             "options": [
                 {
-                    "include-filter": "android.server.wm.ToastWindowTest"
+                    "include-filter": "android.server.wm.window.ToastWindowTest"
                 }
             ],
             "file_patterns": ["INotificationManager\\.aidl"]
@@ -263,6 +268,10 @@
         {
             "file_patterns": ["(/|^)ActivityThreadTest.java"],
             "name": "FrameworksCoreTests"
+        },
+        {
+            "file_patterns": ["(/|^)AppOpsManager.java"],
+            "name": "CtsAppOpsTestCases"
         }
     ]
 }
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 4295517..49543a1 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -87,6 +87,8 @@
             int userId);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS)")
     void clearBroadcastEvents(String callingPackage, int userId);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.DUMP)")
+    boolean isPackageExemptedFromBroadcastResponseStats(String packageName, int userId);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG)")
     String getAppStandbyConstant(String key);
 }
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 4b0ca28..ecf16439 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -1528,6 +1528,22 @@
         }
     }
 
+    /**
+     * Checks whether the given {@code packageName} is exempted from broadcast response tracking.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    @UserHandleAware
+    public boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String packageName) {
+        try {
+            return mService.isPackageExemptedFromBroadcastResponseStats(packageName,
+                    mContext.getUserId());
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     /** @hide */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     @Nullable
diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index c9735b0..86087cb 100644
--- a/core/java/android/content/pm/IShortcutService.aidl
+++ b/core/java/android/content/pm/IShortcutService.aidl
@@ -61,7 +61,7 @@
 
     void resetThrottling(); // system only API for developer opsions
 
-    void onApplicationActive(String packageName, int userId); // system only API for sysUI
+    oneway void onApplicationActive(String packageName, int userId); // system only API for sysUI
 
     byte[] getBackupPayload(int user);
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 3791777..289519d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2373,6 +2373,7 @@
             USER_MIN_ASPECT_RATIO_4_3,
             USER_MIN_ASPECT_RATIO_16_9,
             USER_MIN_ASPECT_RATIO_3_2,
+            USER_MIN_ASPECT_RATIO_FULLSCREEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface UserMinAspectRatio {}
@@ -2394,8 +2395,9 @@
 
     /**
      * Aspect ratio override code: user forces app to the aspect ratio of the device display size.
-     * This will be the portrait aspect ratio of the device if the app is portrait or the landscape
-     * aspect ratio of the device if the app is landscape.
+     * This will be the portrait aspect ratio of the device if the app has fixed portrait
+     * orientation or the landscape aspect ratio of the device if the app has fixed landscape
+     * orientation.
      *
      * @hide
      */
@@ -2419,6 +2421,12 @@
      */
     public static final int USER_MIN_ASPECT_RATIO_3_2 = 5;
 
+    /**
+     * Aspect ratio override code: user forces app to fullscreen
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_FULLSCREEN = 6;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "DELETE_" }, value = {
             DELETE_KEEP_DATA,
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 048289f..508eeed 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1456,8 +1456,8 @@
 
     private static AssetManager newConfiguredAssetManager() {
         AssetManager assetManager = new AssetManager();
-        assetManager.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                Build.VERSION.RESOURCES_SDK_INT);
+        assetManager.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, Build.VERSION.RESOURCES_SDK_INT);
         return assetManager;
     }
 
@@ -9011,8 +9011,8 @@
             }
 
             AssetManager assets = new AssetManager();
-            assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                    Build.VERSION.RESOURCES_SDK_INT);
+            assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, Build.VERSION.RESOURCES_SDK_INT);
             assets.setApkAssets(apkAssets, false /*invalidateCaches*/);
 
             mCachedAssetManager = assets;
@@ -9086,8 +9086,8 @@
 
         private static AssetManager createAssetManagerWithAssets(ApkAssets[] apkAssets) {
             final AssetManager assets = new AssetManager();
-            assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                    Build.VERSION.RESOURCES_SDK_INT);
+            assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                    0, 0, 0, Build.VERSION.RESOURCES_SDK_INT);
             assets.setApkAssets(apkAssets, false /*invalidateCaches*/);
             return assets;
         }
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index b225de4..23b9d0b 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -1480,9 +1480,13 @@
             int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp,
             int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender,
             int majorVersion) {
-        synchronized (this) {
-            ensureValidLocked();
-            nativeSetConfiguration(mObject, mcc, mnc, locale, orientation, touchscreen, density,
+        if (locale != null) {
+            setConfiguration(mcc, mnc, null, new String[]{locale}, orientation, touchscreen,
+                    density, keyboard, keyboardHidden, navigation, screenWidth, screenHeight,
+                    smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode,
+                    colorMode, grammaticalGender, majorVersion);
+        } else {
+            setConfiguration(mcc, mnc, null, null, orientation, touchscreen, density,
                     keyboard, keyboardHidden, navigation, screenWidth, screenHeight,
                     smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode,
                     colorMode, grammaticalGender, majorVersion);
@@ -1490,6 +1494,25 @@
     }
 
     /**
+     * Change the configuration used when retrieving resources.  Not for use by
+     * applications.
+     * @hide
+     */
+    public void setConfiguration(int mcc, int mnc, String defaultLocale, String[] locales,
+            int orientation, int touchscreen, int density, int keyboard, int keyboardHidden,
+            int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp,
+            int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode,
+            int grammaticalGender, int majorVersion) {
+        synchronized (this) {
+            ensureValidLocked();
+            nativeSetConfiguration(mObject, mcc, mnc, defaultLocale, locales, orientation,
+                    touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth,
+                    screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp,
+                    screenLayout, uiMode, colorMode, grammaticalGender, majorVersion);
+        }
+    }
+
+    /**
      * @hide
      */
     @UnsupportedAppUsage
@@ -1572,10 +1595,11 @@
     private static native void nativeSetApkAssets(long ptr, @NonNull ApkAssets[] apkAssets,
             boolean invalidateCaches);
     private static native void nativeSetConfiguration(long ptr, int mcc, int mnc,
-            @Nullable String locale, int orientation, int touchscreen, int density, int keyboard,
-            int keyboardHidden, int navigation, int screenWidth, int screenHeight,
-            int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout,
-            int uiMode, int colorMode, int grammaticalGender, int majorVersion);
+            @Nullable String defaultLocale, @NonNull String[] locales, int orientation,
+            int touchscreen, int density, int keyboard, int keyboardHidden, int navigation,
+            int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp,
+            int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender,
+            int majorVersion);
     private static native @NonNull SparseArray<String> nativeGetAssignedPackageIdentifiers(
             long ptr, boolean includeOverlays, boolean includeLoaders);
 
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 1c8276c..62630c8 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -154,7 +154,6 @@
     /**
      * Current user preference for the grammatical gender.
      */
-    @GrammaticalGender
     private int mGrammaticalGender;
 
     /** @hide */
@@ -167,6 +166,13 @@
     public @interface GrammaticalGender {}
 
     /**
+     * Constant for grammatical gender: to indicate that the grammatical gender is undefined.
+     * Only for internal usage.
+     * @hide
+     */
+    public static final int GRAMMATICAL_GENDER_UNDEFINED = -1;
+
+    /**
      * Constant for grammatical gender: to indicate the user has not specified the terms
      * of address for the application.
      */
@@ -1120,12 +1126,12 @@
         } else {
             sb.append(" ?localeList");
         }
-        if (mGrammaticalGender != 0) {
+        if (mGrammaticalGender > 0) {
             switch (mGrammaticalGender) {
                 case GRAMMATICAL_GENDER_NEUTRAL: sb.append(" neuter"); break;
                 case GRAMMATICAL_GENDER_FEMININE: sb.append(" feminine"); break;
                 case GRAMMATICAL_GENDER_MASCULINE: sb.append(" masculine"); break;
-                case GRAMMATICAL_GENDER_NOT_SPECIFIED: sb.append(" ?grgend"); break;
+                default: sb.append(" ?grgend"); break;
             }
         }
         int layoutDir = (screenLayout&SCREENLAYOUT_LAYOUTDIR_MASK);
@@ -1570,7 +1576,7 @@
         seq = 0;
         windowConfiguration.setToDefaults();
         fontWeightAdjustment = FONT_WEIGHT_ADJUSTMENT_UNDEFINED;
-        mGrammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED;
+        mGrammaticalGender = GRAMMATICAL_GENDER_UNDEFINED;
     }
 
     /**
@@ -1773,7 +1779,8 @@
             changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT;
             fontWeightAdjustment = delta.fontWeightAdjustment;
         }
-        if (delta.mGrammaticalGender != mGrammaticalGender) {
+        if (delta.mGrammaticalGender != GRAMMATICAL_GENDER_UNDEFINED
+                && delta.mGrammaticalGender != mGrammaticalGender) {
             changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER;
             mGrammaticalGender = delta.mGrammaticalGender;
         }
@@ -1998,7 +2005,8 @@
             changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT;
         }
 
-        if (mGrammaticalGender != delta.mGrammaticalGender) {
+        if ((compareUndefined || delta.mGrammaticalGender != GRAMMATICAL_GENDER_UNDEFINED)
+                && mGrammaticalGender != delta.mGrammaticalGender) {
             changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER;
         }
         return changed;
@@ -2284,6 +2292,17 @@
      */
     @GrammaticalGender
     public int getGrammaticalGender() {
+        return mGrammaticalGender == GRAMMATICAL_GENDER_UNDEFINED
+                ? GRAMMATICAL_GENDER_NOT_SPECIFIED : mGrammaticalGender;
+    }
+
+    /**
+     * Internal getter of grammatical gender, to get the raw value of grammatical gender,
+     * which include {@link #GRAMMATICAL_GENDER_UNDEFINED}.
+     * @hide
+     */
+
+    public int getGrammaticalGenderRaw() {
         return mGrammaticalGender;
     }
 
@@ -2972,7 +2991,7 @@
         configOut.fontWeightAdjustment = XmlUtils.readIntAttribute(parser,
                 XML_ATTR_FONT_WEIGHT_ADJUSTMENT, FONT_WEIGHT_ADJUSTMENT_UNDEFINED);
         configOut.mGrammaticalGender = XmlUtils.readIntAttribute(parser,
-                XML_ATTR_GRAMMATICAL_GENDER, GRAMMATICAL_GENDER_NOT_SPECIFIED);
+                XML_ATTR_GRAMMATICAL_GENDER, GRAMMATICAL_GENDER_UNDEFINED);
 
         // For persistence, we don't care about assetsSeq and WindowConfiguration, so do not read it
         // out.
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index 62a46b6..3e0ab90 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -16,6 +16,8 @@
 
 package android.content.res;
 
+import static android.os.SystemProperties.PROP_VALUE_MAX;
+
 import android.annotation.NonNull;
 import android.util.Pools.SimplePool;
 
@@ -23,9 +25,6 @@
 
 import com.android.internal.R;
 
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
 /**
  * Defines the string attribute length and child tag count restrictions for a xml element.
  *
@@ -34,8 +33,13 @@
 public class Element {
     private static final int DEFAULT_MAX_STRING_ATTR_LENGTH = 32_768;
     private static final int MAX_POOL_SIZE = 128;
-
-    private static final String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android";
+    private static final int MAX_ATTR_LEN_URL_COMPONENT = 256;
+    private static final int MAX_ATTR_LEN_PERMISSION_GROUP = 256;
+    private static final int MAX_ATTR_LEN_PACKAGE = 256;
+    private static final int MAX_ATTR_LEN_MIMETYPE = 512;
+    public static final int MAX_ATTR_LEN_NAME = 1024;
+    public static final int MAX_ATTR_LEN_PATH = 4000;
+    public static final int MAX_ATTR_LEN_DATA_VALUE = 4000;
 
     protected static final String TAG_ACTION = "action";
     protected static final String TAG_ACTIVITY = "activity";
@@ -123,43 +127,9 @@
     protected static final String TAG_ATTR_VERSION_NAME = "versionName";
     protected static final String TAG_ATTR_WRITE_PERMISSION = "writePermission";
 
-    private static final String[] ACTIVITY_STR_ATTR_NAMES = {TAG_ATTR_NAME,
-            TAG_ATTR_PARENT_ACTIVITY_NAME, TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS,
-            TAG_ATTR_TASK_AFFINITY};
-    private static final String[] ACTIVITY_ALIAS_STR_ATTR_NAMES = {TAG_ATTR_NAME,
-            TAG_ATTR_PERMISSION, TAG_ATTR_TARGET_ACTIVITY};
-    private static final String[] APPLICATION_STR_ATTR_NAMES = {TAG_ATTR_BACKUP_AGENT,
-            TAG_ATTR_MANAGE_SPACE_ACTIVITY, TAG_ATTR_NAME, TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS,
-            TAG_ATTR_REQUIRED_ACCOUNT_TYPE, TAG_ATTR_RESTRICTED_ACCOUNT_TYPE,
-            TAG_ATTR_TASK_AFFINITY};
-    private static final String[] DATA_STR_ATTR_NAMES = {TAG_ATTR_SCHEME, TAG_ATTR_HOST,
-            TAG_ATTR_PORT, TAG_ATTR_PATH, TAG_ATTR_PATH_PATTERN, TAG_ATTR_PATH_PREFIX,
-            TAG_ATTR_PATH_SUFFIX, TAG_ATTR_PATH_ADVANCED_PATTERN, TAG_ATTR_MIMETYPE};
-    private static final String[] GRANT_URI_PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_PATH,
-            TAG_ATTR_PATH_PATTERN, TAG_ATTR_PATH_PREFIX};
-    private static final String[] INSTRUMENTATION_STR_ATTR_NAMES = {TAG_ATTR_NAME,
-            TAG_ATTR_TARGET_PACKAGE, TAG_ATTR_TARGET_PROCESSES};
-    private static final String[] MANIFEST_STR_ATTR_NAMES = {TAG_ATTR_PACKAGE,
-            TAG_ATTR_SHARED_USER_ID, TAG_ATTR_VERSION_NAME};
-    private static final String[] OVERLAY_STR_ATTR_NAMES = {TAG_ATTR_CATEGORY,
-            TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME, TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE,
-            TAG_ATTR_TARGET_PACKAGE, TAG_ATTR_TARGET_NAME};
-    private static final String[] PATH_PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_PATH,
-            TAG_ATTR_PATH_PREFIX, TAG_ATTR_PATH_PATTERN, TAG_ATTR_PERMISSION,
-            TAG_ATTR_READ_PERMISSION, TAG_ATTR_WRITE_PERMISSION};
-    private static final String[] PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_NAME,
-            TAG_ATTR_PERMISSION_GROUP};
-    private static final String[] PROVIDER_STR_ATTR_NAMES = {TAG_ATTR_NAME, TAG_ATTR_PERMISSION,
-            TAG_ATTR_PROCESS, TAG_ATTR_READ_PERMISSION, TAG_ATTR_WRITE_PERMISSION};
-    private static final String[] RECEIVER_SERVICE_STR_ATTR_NAMES = {TAG_ATTR_NAME,
-            TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS};
-    private static final String[] NAME_ATTR = {TAG_ATTR_NAME};
-    private static final String[] NAME_VALUE_ATTRS = {TAG_ATTR_NAME, TAG_ATTR_VALUE};
-
-    private String[] mStringAttrNames = new String[0];
     // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new
     // tags are added then the size here should be increased to match.
-    private final TagCounter[] mTagCounters = new TagCounter[35];
+    private final TagCounter[] mTagCounters = new TagCounter[34];
 
     String mTag;
 
@@ -177,7 +147,6 @@
     }
 
     void recycle() {
-        mStringAttrNames = new String[0];
         mTag = null;
         sPool.get().release(this);
     }
@@ -230,33 +199,79 @@
                 return 20;
             case TAG_USES_CONFIGURATION:
                 return 21;
-            case TAG_USES_PERMISSION_SDK_23:
-                return 22;
             case TAG_USES_SDK:
-                return 23;
+                return 22;
             case TAG_COMPATIBLE_SCREENS:
-                return 24;
+                return 23;
             case TAG_QUERIES:
-                return 25;
+                return 24;
             case TAG_ATTRIBUTION:
-                return 26;
+                return 25;
             case TAG_USES_FEATURE:
-                return 27;
+                return 26;
             case TAG_PERMISSION:
-                return 28;
+                return 27;
             case TAG_USES_PERMISSION:
-                return 29;
+            case TAG_USES_PERMISSION_SDK_23:
+            case TAG_USES_PERMISSION_SDK_M:
+                return 28;
             case TAG_GRANT_URI_PERMISSION:
-                return 30;
+                return 29;
             case TAG_PATH_PERMISSION:
-                return 31;
+                return 30;
             case TAG_PACKAGE:
-                return 32;
+                return 31;
             case TAG_INTENT:
-                return 33;
+                return 32;
             default:
                 // The size of the mTagCounters array should be equal to this value+1
-                return 34;
+                return 33;
+        }
+    }
+
+    static boolean shouldValidate(String tag) {
+        switch (tag) {
+            case TAG_ACTION:
+            case TAG_ACTIVITY:
+            case TAG_ACTIVITY_ALIAS:
+            case TAG_APPLICATION:
+            case TAG_ATTRIBUTION:
+            case TAG_CATEGORY:
+            case TAG_COMPATIBLE_SCREENS:
+            case TAG_DATA:
+            case TAG_GRANT_URI_PERMISSION:
+            case TAG_INSTRUMENTATION:
+            case TAG_INTENT:
+            case TAG_INTENT_FILTER:
+            case TAG_LAYOUT:
+            case TAG_MANIFEST:
+            case TAG_META_DATA:
+            case TAG_OVERLAY:
+            case TAG_PACKAGE:
+            case TAG_PATH_PERMISSION:
+            case TAG_PERMISSION:
+            case TAG_PERMISSION_GROUP:
+            case TAG_PERMISSION_TREE:
+            case TAG_PROFILEABLE:
+            case TAG_PROPERTY:
+            case TAG_PROVIDER:
+            case TAG_QUERIES:
+            case TAG_RECEIVER:
+            case TAG_SCREEN:
+            case TAG_SERVICE:
+            case TAG_SUPPORTS_GL_TEXTURE:
+            case TAG_SUPPORTS_SCREENS:
+            case TAG_USES_CONFIGURATION:
+            case TAG_USES_FEATURE:
+            case TAG_USES_LIBRARY:
+            case TAG_USES_NATIVE_LIBRARY:
+            case TAG_USES_PERMISSION:
+            case TAG_USES_PERMISSION_SDK_23:
+            case TAG_USES_PERMISSION_SDK_M:
+            case TAG_USES_SDK:
+                return true;
+            default:
+                return false;
         }
     }
 
@@ -264,55 +279,31 @@
         this.mTag = tag;
         mChildTagMask = 0;
         switch (tag) {
-            case TAG_ACTION:
-            case TAG_CATEGORY:
-            case TAG_PACKAGE:
-            case TAG_PERMISSION_GROUP:
-            case TAG_PERMISSION_TREE:
-            case TAG_SUPPORTS_GL_TEXTURE:
-            case TAG_USES_FEATURE:
-            case TAG_USES_LIBRARY:
-            case TAG_USES_NATIVE_LIBRARY:
-            case TAG_USES_PERMISSION:
-            case TAG_USES_PERMISSION_SDK_23:
-            case TAG_USES_SDK:
-                setStringAttrNames(NAME_ATTR);
-                break;
             case TAG_ACTIVITY:
-                setStringAttrNames(ACTIVITY_STR_ATTR_NAMES);
                 initializeCounter(TAG_LAYOUT, 1000);
-                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_META_DATA, 1000);
                 initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_ACTIVITY_ALIAS:
-                setStringAttrNames(ACTIVITY_ALIAS_STR_ATTR_NAMES);
-                initializeCounter(TAG_META_DATA, 8000);
+            case TAG_RECEIVER:
+            case TAG_SERVICE:
+                initializeCounter(TAG_META_DATA, 1000);
                 initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_APPLICATION:
-                setStringAttrNames(APPLICATION_STR_ATTR_NAMES);
                 initializeCounter(TAG_PROFILEABLE, 100);
                 initializeCounter(TAG_USES_NATIVE_LIBRARY, 100);
                 initializeCounter(TAG_RECEIVER, 1000);
                 initializeCounter(TAG_SERVICE, 1000);
+                initializeCounter(TAG_META_DATA, 1000);
+                initializeCounter(TAG_USES_LIBRARY, 1000);
                 initializeCounter(TAG_ACTIVITY_ALIAS, 4000);
-                initializeCounter(TAG_USES_LIBRARY, 4000);
                 initializeCounter(TAG_PROVIDER, 8000);
-                initializeCounter(TAG_META_DATA, 8000);
                 initializeCounter(TAG_ACTIVITY, 40000);
                 break;
             case TAG_COMPATIBLE_SCREENS:
                 initializeCounter(TAG_SCREEN, 4000);
                 break;
-            case TAG_DATA:
-                setStringAttrNames(DATA_STR_ATTR_NAMES);
-                break;
-            case TAG_GRANT_URI_PERMISSION:
-                setStringAttrNames(GRANT_URI_PERMISSION_STR_ATTR_NAMES);
-                break;
-            case TAG_INSTRUMENTATION:
-                setStringAttrNames(INSTRUMENTATION_STR_ATTR_NAMES);
-                break;
             case TAG_INTENT:
             case TAG_INTENT_FILTER:
                 initializeCounter(TAG_ACTION, 20000);
@@ -320,7 +311,6 @@
                 initializeCounter(TAG_DATA, 40000);
                 break;
             case TAG_MANIFEST:
-                setStringAttrNames(MANIFEST_STR_ATTR_NAMES);
                 initializeCounter(TAG_APPLICATION, 100);
                 initializeCounter(TAG_OVERLAY, 100);
                 initializeCounter(TAG_INSTRUMENTATION, 100);
@@ -329,7 +319,6 @@
                 initializeCounter(TAG_SUPPORTS_GL_TEXTURE, 100);
                 initializeCounter(TAG_SUPPORTS_SCREENS, 100);
                 initializeCounter(TAG_USES_CONFIGURATION, 100);
-                initializeCounter(TAG_USES_PERMISSION_SDK_23, 100);
                 initializeCounter(TAG_USES_SDK, 100);
                 initializeCounter(TAG_COMPATIBLE_SCREENS, 200);
                 initializeCounter(TAG_QUERIES, 200);
@@ -338,24 +327,10 @@
                 initializeCounter(TAG_PERMISSION, 2000);
                 initializeCounter(TAG_USES_PERMISSION, 20000);
                 break;
-            case TAG_META_DATA:
-            case TAG_PROPERTY:
-                setStringAttrNames(NAME_VALUE_ATTRS);
-                break;
-            case TAG_OVERLAY:
-                setStringAttrNames(OVERLAY_STR_ATTR_NAMES);
-                break;
-            case TAG_PATH_PERMISSION:
-                setStringAttrNames(PATH_PERMISSION_STR_ATTR_NAMES);
-                break;
-            case TAG_PERMISSION:
-                setStringAttrNames(PERMISSION_STR_ATTR_NAMES);
-                break;
             case TAG_PROVIDER:
-                setStringAttrNames(PROVIDER_STR_ATTR_NAMES);
                 initializeCounter(TAG_GRANT_URI_PERMISSION, 100);
                 initializeCounter(TAG_PATH_PERMISSION, 100);
-                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_META_DATA, 1000);
                 initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_QUERIES:
@@ -363,39 +338,23 @@
                 initializeCounter(TAG_INTENT, 2000);
                 initializeCounter(TAG_PROVIDER, 8000);
                 break;
-            case TAG_RECEIVER:
-            case TAG_SERVICE:
-                setStringAttrNames(RECEIVER_SERVICE_STR_ATTR_NAMES);
-                initializeCounter(TAG_META_DATA, 8000);
-                initializeCounter(TAG_INTENT_FILTER, 20000);
-                break;
         }
     }
 
-    private void setStringAttrNames(String[] attrNames) {
-        mStringAttrNames = attrNames;
-    }
-
-    private static String getAttrNamespace(String attrName) {
-        if (attrName.equals(TAG_ATTR_PACKAGE)) {
-            return null;
-        }
-        return ANDROID_NAMESPACE;
-    }
-
-    private static int getAttrStringMaxLength(String attrName) {
+    private static int getAttrStrMaxLen(String attrName) {
         switch (attrName) {
             case TAG_ATTR_HOST:
-            case TAG_ATTR_PACKAGE:
-            case TAG_ATTR_PERMISSION_GROUP:
             case TAG_ATTR_PORT:
-            case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE:
             case TAG_ATTR_SCHEME:
+                return MAX_ATTR_LEN_URL_COMPONENT;
+            case TAG_ATTR_PERMISSION_GROUP:
+                return MAX_ATTR_LEN_PERMISSION_GROUP;
             case TAG_ATTR_SHARED_USER_ID:
+            case TAG_ATTR_PACKAGE:
             case TAG_ATTR_TARGET_PACKAGE:
-                return 256;
+                return MAX_ATTR_LEN_PACKAGE;
             case TAG_ATTR_MIMETYPE:
-                return 512;
+                return MAX_ATTR_LEN_MIMETYPE;
             case TAG_ATTR_BACKUP_AGENT:
             case TAG_ATTR_CATEGORY:
             case TAG_ATTR_MANAGE_SPACE_ACTIVITY:
@@ -405,33 +364,343 @@
             case TAG_ATTR_PROCESS:
             case TAG_ATTR_READ_PERMISSION:
             case TAG_ATTR_REQUIRED_ACCOUNT_TYPE:
+            case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME:
             case TAG_ATTR_RESTRICTED_ACCOUNT_TYPE:
             case TAG_ATTR_TARGET_ACTIVITY:
             case TAG_ATTR_TARGET_NAME:
             case TAG_ATTR_TARGET_PROCESSES:
             case TAG_ATTR_TASK_AFFINITY:
             case TAG_ATTR_WRITE_PERMISSION:
-                return 1024;
+            case TAG_ATTR_VERSION_NAME:
+                return MAX_ATTR_LEN_NAME;
             case TAG_ATTR_PATH:
             case TAG_ATTR_PATH_ADVANCED_PATTERN:
             case TAG_ATTR_PATH_PATTERN:
             case TAG_ATTR_PATH_PREFIX:
             case TAG_ATTR_PATH_SUFFIX:
-            case TAG_ATTR_VERSION_NAME:
-                return 4000;
+                return MAX_ATTR_LEN_PATH;
+            case TAG_ATTR_VALUE:
+                return MAX_ATTR_LEN_DATA_VALUE;
+            case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE:
+                return PROP_VALUE_MAX;
             default:
                 return DEFAULT_MAX_STRING_ATTR_LENGTH;
         }
     }
 
-    private static int getResStringMaxLength(@StyleableRes int index) {
+    private int getResStrMaxLen(@StyleableRes int index) {
+        switch (mTag) {
+            case TAG_ACTION:
+                return getActionResStrMaxLen(index);
+            case TAG_ACTIVITY:
+                return getActivityResStrMaxLen(index);
+            case TAG_ACTIVITY_ALIAS:
+                return getActivityAliasResStrMaxLen(index);
+            case TAG_APPLICATION:
+                return getApplicationResStrMaxLen(index);
+            case TAG_DATA:
+                return getDataResStrMaxLen(index);
+            case TAG_CATEGORY:
+                return getCategoryResStrMaxLen(index);
+            case TAG_GRANT_URI_PERMISSION:
+                return getGrantUriPermissionResStrMaxLen(index);
+            case TAG_INSTRUMENTATION:
+                return getInstrumentationResStrMaxLen(index);
+            case TAG_MANIFEST:
+                return getManifestResStrMaxLen(index);
+            case TAG_META_DATA:
+                return getMetaDataResStrMaxLen(index);
+            case TAG_OVERLAY:
+                return getOverlayResStrMaxLen(index);
+            case TAG_PATH_PERMISSION:
+                return getPathPermissionResStrMaxLen(index);
+            case TAG_PERMISSION:
+                return getPermissionResStrMaxLen(index);
+            case TAG_PERMISSION_GROUP:
+                return getPermissionGroupResStrMaxLen(index);
+            case TAG_PERMISSION_TREE:
+                return getPermissionTreeResStrMaxLen(index);
+            case TAG_PROPERTY:
+                return getPropertyResStrMaxLen(index);
+            case TAG_PROVIDER:
+                return getProviderResStrMaxLen(index);
+            case TAG_RECEIVER:
+                return getReceiverResStrMaxLen(index);
+            case TAG_SERVICE:
+                return getServiceResStrMaxLen(index);
+            case TAG_USES_FEATURE:
+                return getUsesFeatureResStrMaxLen(index);
+            case TAG_USES_LIBRARY:
+                return getUsesLibraryResStrMaxLen(index);
+            case TAG_USES_NATIVE_LIBRARY:
+                return getUsesNativeLibraryResStrMaxLen(index);
+            case TAG_USES_PERMISSION:
+            case TAG_USES_PERMISSION_SDK_23:
+            case TAG_USES_PERMISSION_SDK_M:
+                return getUsesPermissionResStrMaxLen(index);
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getActionResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestAction_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getActivityResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestActivity_name:
+            case R.styleable.AndroidManifestActivity_parentActivityName:
+            case R.styleable.AndroidManifestActivity_permission:
+            case R.styleable.AndroidManifestActivity_process:
+            case R.styleable.AndroidManifestActivity_taskAffinity:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getActivityAliasResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestActivityAlias_name:
+            case R.styleable.AndroidManifestActivityAlias_permission:
+            case R.styleable.AndroidManifestActivityAlias_targetActivity:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getApplicationResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestApplication_backupAgent:
+            case R.styleable.AndroidManifestApplication_manageSpaceActivity:
+            case R.styleable.AndroidManifestApplication_name:
+            case R.styleable.AndroidManifestApplication_permission:
+            case R.styleable.AndroidManifestApplication_process:
+            case R.styleable.AndroidManifestApplication_requiredAccountType:
+            case R.styleable.AndroidManifestApplication_restrictedAccountType:
+            case R.styleable.AndroidManifestApplication_taskAffinity:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getCategoryResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestCategory_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getDataResStrMaxLen(@StyleableRes int index) {
         switch (index) {
             case R.styleable.AndroidManifestData_host:
             case R.styleable.AndroidManifestData_port:
             case R.styleable.AndroidManifestData_scheme:
-                return 255;
+                return MAX_ATTR_LEN_URL_COMPONENT;
             case R.styleable.AndroidManifestData_mimeType:
-                return 512;
+                return MAX_ATTR_LEN_MIMETYPE;
+            case R.styleable.AndroidManifestData_path:
+            case R.styleable.AndroidManifestData_pathPattern:
+            case R.styleable.AndroidManifestData_pathPrefix:
+            case R.styleable.AndroidManifestData_pathSuffix:
+            case R.styleable.AndroidManifestData_pathAdvancedPattern:
+                return MAX_ATTR_LEN_PATH;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getGrantUriPermissionResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestGrantUriPermission_path:
+            case R.styleable.AndroidManifestGrantUriPermission_pathPattern:
+            case R.styleable.AndroidManifestGrantUriPermission_pathPrefix:
+                return MAX_ATTR_LEN_PATH;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getInstrumentationResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestInstrumentation_targetPackage:
+                return MAX_ATTR_LEN_PACKAGE;
+            case R.styleable.AndroidManifestInstrumentation_name:
+            case R.styleable.AndroidManifestInstrumentation_targetProcesses:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getManifestResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifest_sharedUserId:
+                return MAX_ATTR_LEN_PACKAGE;
+            case R.styleable.AndroidManifest_versionName:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getMetaDataResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestMetaData_name:
+                return MAX_ATTR_LEN_NAME;
+            case R.styleable.AndroidManifestMetaData_value:
+                return MAX_ATTR_LEN_DATA_VALUE;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getOverlayResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestResourceOverlay_targetPackage:
+                return MAX_ATTR_LEN_PACKAGE;
+            case R.styleable.AndroidManifestResourceOverlay_category:
+            case R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName:
+            case R.styleable.AndroidManifestResourceOverlay_targetName:
+                return MAX_ATTR_LEN_NAME;
+            case R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue:
+                return PROP_VALUE_MAX;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getPathPermissionResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestPathPermission_permission:
+            case R.styleable.AndroidManifestPathPermission_readPermission:
+            case R.styleable.AndroidManifestPathPermission_writePermission:
+                return MAX_ATTR_LEN_NAME;
+            case R.styleable.AndroidManifestPathPermission_path:
+            case R.styleable.AndroidManifestPathPermission_pathPattern:
+            case R.styleable.AndroidManifestPathPermission_pathPrefix:
+                return MAX_ATTR_LEN_PATH;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getPermissionResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestPermission_permissionGroup:
+                return MAX_ATTR_LEN_PERMISSION_GROUP;
+            case R.styleable.AndroidManifestPermission_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getPermissionGroupResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestPermissionGroup_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getPermissionTreeResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestPermissionTree_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getPropertyResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestProperty_name:
+                return MAX_ATTR_LEN_NAME;
+            case R.styleable.AndroidManifestProperty_value:
+                return MAX_ATTR_LEN_DATA_VALUE;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getProviderResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestProvider_name:
+            case R.styleable.AndroidManifestProvider_permission:
+            case R.styleable.AndroidManifestProvider_process:
+            case R.styleable.AndroidManifestProvider_readPermission:
+            case R.styleable.AndroidManifestProvider_writePermission:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getReceiverResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestReceiver_name:
+            case R.styleable.AndroidManifestReceiver_permission:
+            case R.styleable.AndroidManifestReceiver_process:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getServiceResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestReceiver_name:
+            case R.styleable.AndroidManifestReceiver_permission:
+            case R.styleable.AndroidManifestReceiver_process:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getUsesFeatureResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestUsesFeature_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getUsesLibraryResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestUsesLibrary_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getUsesNativeLibraryResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestUsesNativeLibrary_name:
+                return MAX_ATTR_LEN_NAME;
+            default:
+                return DEFAULT_MAX_STRING_ATTR_LENGTH;
+        }
+    }
+
+    private static int getUsesPermissionResStrMaxLen(@StyleableRes int index) {
+        switch (index) {
+            case R.styleable.AndroidManifestUsesPermission_name:
+                return MAX_ATTR_LEN_NAME;
             default:
                 return DEFAULT_MAX_STRING_ATTR_LENGTH;
         }
@@ -450,31 +719,25 @@
         return (mChildTagMask & (1 << getCounterIdx(tag))) != 0;
     }
 
-    void validateStringAttrs(@NonNull XmlPullParser attrs) throws XmlPullParserException {
-        for (int i = 0; i < mStringAttrNames.length; i++) {
-            String attrName = mStringAttrNames[i];
-            String val = attrs.getAttributeValue(getAttrNamespace(attrName), attrName);
-            if (val != null && val.length() > getAttrStringMaxLength(attrName)) {
-                throw new XmlPullParserException("String length limit exceeded for "
-                        + "attribute " + attrName + " in " + mTag);
-            }
+    void validateStrAttr(String attrName, String attrValue) {
+        if (attrValue != null && attrValue.length() > getAttrStrMaxLen(attrName)) {
+            throw new SecurityException("String length limit exceeded for attribute " + attrName
+                    + " in " + mTag);
         }
     }
 
-    void validateResStringAttr(@StyleableRes int index, CharSequence stringValue)
-            throws XmlPullParserException {
-        if (stringValue != null && stringValue.length() > getResStringMaxLength(index)) {
-            throw new XmlPullParserException("String length limit exceeded for "
-                    + "attribute in " + mTag);
+    void validateResStrAttr(@StyleableRes int index, CharSequence stringValue) {
+        if (stringValue != null && stringValue.length() > getResStrMaxLen(index)) {
+            throw new SecurityException("String length limit exceeded for attribute in " + mTag);
         }
     }
 
-    void seen(@NonNull Element element) throws XmlPullParserException {
+    void seen(@NonNull Element element) {
         TagCounter counter = mTagCounters[getCounterIdx(element.mTag)];
         if (counter != null) {
             counter.increment();
             if (!counter.isValid()) {
-                throw new XmlPullParserException("The number of child " + element.mTag
+                throw new SecurityException("The number of child " + element.mTag
                         + " elements exceeded the max allowed in " + this.mTag);
             }
         }
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index 61d5aa6..f2468b5 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -407,14 +407,12 @@
                     mConfiguration.setLocales(locales);
                 }
 
+                String[] selectedLocales = null;
+                String defaultLocale = null;
                 if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
                     if (locales.size() > 1) {
                         String[] availableLocales;
-
-                        LocaleList localeList = ResourcesManager.getInstance().getLocaleList();
-                        if (!localeList.isEmpty()) {
-                            availableLocales = localeList.toLanguageTags().split(",");
-                        } else {
+                        if (ResourcesManager.getInstance().getLocaleList().isEmpty()) {
                             // The LocaleList has changed. We must query the AssetManager's
                             // available Locales and figure out the best matching Locale in the new
                             // LocaleList.
@@ -426,16 +424,32 @@
                                     availableLocales = null;
                                 }
                             }
-                        }
-                        if (availableLocales != null) {
-                            final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(
-                                    availableLocales);
-                            if (bestLocale != null && bestLocale != locales.get(0)) {
-                                mConfiguration.setLocales(new LocaleList(bestLocale, locales));
+                            if (availableLocales != null) {
+                                final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(
+                                        availableLocales);
+                                if (bestLocale != null) {
+                                    selectedLocales = new String[]{
+                                            adjustLanguageTag(bestLocale.toLanguageTag())};
+                                    if (!bestLocale.equals(locales.get(0))) {
+                                        mConfiguration.setLocales(
+                                                new LocaleList(bestLocale, locales));
+                                    }
+                                }
                             }
+                        } else {
+                            selectedLocales = locales.getIntersection(
+                                    ResourcesManager.getInstance().getLocaleList());
+                            defaultLocale = ResourcesManager.getInstance()
+                                    .getLocaleList().get(0).toLanguageTag();
                         }
                     }
                 }
+                if (selectedLocales == null) {
+                    selectedLocales = new String[locales.size()];
+                    for (int i = 0; i < locales.size(); i++) {
+                        selectedLocales[i] = adjustLanguageTag(locales.get(i).toLanguageTag());
+                    }
+                }
 
                 if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
                     mMetrics.densityDpi = mConfiguration.densityDpi;
@@ -470,7 +484,8 @@
                 }
 
                 mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
-                        adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
+                        defaultLocale,
+                        selectedLocales,
                         mConfiguration.orientation,
                         mConfiguration.touchscreen,
                         mConfiguration.densityDpi, mConfiguration.keyboard,
diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java
index 2e84636..48adfb9 100644
--- a/core/java/android/content/res/TypedArray.java
+++ b/core/java/android/content/res/TypedArray.java
@@ -1393,16 +1393,17 @@
     private CharSequence loadStringValueAt(int index) {
         final int[] data = mData;
         final int cookie = data[index + STYLE_ASSET_COOKIE];
+        CharSequence value = null;
         if (cookie < 0) {
             if (mXml != null) {
-                return mXml.getPooledString(data[index + STYLE_DATA]);
+                value = mXml.getPooledString(data[index + STYLE_DATA]);
             }
-            return null;
+        } else {
+            value = mAssets.getPooledStringForCookie(cookie, data[index + STYLE_DATA]);
         }
-        CharSequence value = mAssets.getPooledStringForCookie(cookie, data[index + STYLE_DATA]);
-        if (mXml != null && mXml.mValidator != null) {
+        if (value != null && mXml != null && mXml.mValidator != null) {
             try {
-                mXml.mValidator.validateAttr(mXml, index, value);
+                mXml.mValidator.validateResStrAttr(mXml, index / STYLE_NUM_ENTRIES, value);
             } catch (XmlPullParserException e) {
                 throw new RuntimeException("Failed to validate resource string: " + e.getMessage());
             }
diff --git a/core/java/android/content/res/Validator.java b/core/java/android/content/res/Validator.java
index 8b5e6c6..cae353b 100644
--- a/core/java/android/content/res/Validator.java
+++ b/core/java/android/content/res/Validator.java
@@ -16,9 +16,8 @@
 
 package android.content.res;
 
-import static android.content.res.Element.TAG_MANIFEST;
-
 import android.annotation.NonNull;
+import android.annotation.StyleableRes;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -55,24 +54,19 @@
             return;
         }
         if (eventType == XmlPullParser.START_TAG) {
-            try {
-                String tag = parser.getName();
-                // only validate manifests
-                if (depth == 0 && mElements.size() == 0 && !TAG_MANIFEST.equals(tag)) {
-                    return;
-                }
+            String tag = parser.getName();
+            if (Element.shouldValidate(tag)) {
+                Element element = Element.obtain(tag);
                 Element parent = mElements.peek();
-                if (parent == null || parent.hasChild(tag)) {
-                    Element element = Element.obtain(tag);
-                    element.validateStringAttrs(parser);
-                    if (parent != null) {
+                if (parent != null && parent.hasChild(tag)) {
+                    try {
                         parent.seen(element);
+                    } catch (SecurityException e) {
+                        cleanUp();
+                        throw e;
                     }
-                    mElements.push(element);
                 }
-            } catch (XmlPullParserException e) {
-                cleanUp();
-                throw e;
+                mElements.push(element);
             }
         } else if (eventType == XmlPullParser.END_TAG && depth == mElements.size()) {
             mElements.pop().recycle();
@@ -84,11 +78,21 @@
     /**
      * Validates the resource string of a manifest tag attribute.
      */
-    public void validateAttr(@NonNull XmlPullParser parser, int index, CharSequence stringValue)
-            throws XmlPullParserException {
+    public void validateResStrAttr(@NonNull XmlPullParser parser, @StyleableRes int index,
+            CharSequence stringValue) throws XmlPullParserException {
         if (parser.getDepth() > mElements.size()) {
             return;
         }
-        mElements.peek().validateResStringAttr(index, stringValue);
+        mElements.peek().validateResStrAttr(index, stringValue);
+    }
+
+    /**
+     * Validates the string of a manifest tag attribute by name.
+     */
+    public void validateStrAttr(@NonNull XmlPullParser parser, String attrName, String attrValue) {
+        if (parser.getDepth() > mElements.size()) {
+            return;
+        }
+        mElements.peek().validateStrAttr(attrName, attrValue);
     }
 }
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index 3afc830..7649b32 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -319,7 +319,11 @@
                         "Namespace=" + getAttributeNamespace(idx)
                         + "Name=" + getAttributeName(idx)
                         + ", Value=" + getAttributeValue(idx));
-                return getAttributeValue(idx);
+                String value = getAttributeValue(idx);
+                if (mValidator != null) {
+                    mValidator.validateStrAttr(this, name, value);
+                }
+                return value;
             }
             return null;
         }
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index b7489229..706e75e 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -113,9 +113,6 @@
     private final boolean mIsReadOnlyConnection;
     private PreparedStatement mPreparedStatementPool;
 
-    // A lock access to the statement cache.
-    private final Object mCacheLock = new Object();
-    @GuardedBy("mCacheLock")
     private final PreparedStatementCache mPreparedStatementCache;
 
     // The recent operations log.
@@ -596,9 +593,7 @@
         mConfiguration.updateParametersFrom(configuration);
 
         // Update prepared statement cache size.
-        synchronized (mCacheLock) {
-            mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
-        }
+        mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
 
         if (foreignKeyModeChanged) {
             setForeignKeyModeFromConfiguration();
@@ -630,12 +625,12 @@
         mOnlyAllowReadOnlyOperations = readOnly;
     }
 
-    // Called by SQLiteConnectionPool only.
-    // Returns true if the prepared statement cache contains the specified SQL.
+    // Called by SQLiteConnectionPool only to decide if this connection has the desired statement
+    // already prepared.  Returns true if the prepared statement cache contains the specified SQL.
+    // The statement may be stale, but that will be a rare occurrence and affects performance only
+    // a tiny bit, and only when database schema changes.
     boolean isPreparedStatementInCache(String sql) {
-        synchronized (mCacheLock) {
-            return mPreparedStatementCache.get(sql) != null;
-        }
+        return mPreparedStatementCache.get(sql) != null;
     }
 
     /**
@@ -1070,28 +1065,41 @@
     /**
      * Return a {@link #PreparedStatement}, possibly from the cache.
      */
-    @GuardedBy("mCacheLock")
     private PreparedStatement acquirePreparedStatementLI(String sql) {
         ++mPool.mTotalPrepareStatements;
-        PreparedStatement statement = mPreparedStatementCache.get(sql);
+        PreparedStatement statement = mPreparedStatementCache.getStatement(sql);
+        long seqNum = mPreparedStatementCache.getLastSeqNum();
+
         boolean skipCache = false;
         if (statement != null) {
             if (!statement.mInUse) {
-                statement.mInUse = true;
-                return statement;
+                if (statement.mSeqNum == seqNum) {
+                    // This is a valid statement.  Claim it and return it.
+                    statement.mInUse = true;
+                    return statement;
+                } else {
+                    // This is a stale statement.  Remove it from the cache.  Treat this as if the
+                    // statement was never found, which means we should not skip the cache.
+                    mPreparedStatementCache.remove(sql);
+                    statement = null;
+                    // Leave skipCache == false.
+                }
+            } else {
+                // The statement is already in the cache but is in use (this statement appears to
+                // be not only re-entrant but recursive!).  So prepare a new copy of the statement
+                // but do not cache it.
+                skipCache = true;
             }
-            // The statement is already in the cache but is in use (this statement appears
-            // to be not only re-entrant but recursive!).  So prepare a new copy of the
-            // statement but do not cache it.
-            skipCache = true;
         }
         ++mPool.mTotalPrepareStatementCacheMiss;
-        final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
+        final long statementPtr = mPreparedStatementCache.createStatement(sql);
+        seqNum = mPreparedStatementCache.getLastSeqNum();
         try {
             final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
             final int type = DatabaseUtils.getSqlStatementType(sql);
             final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
-            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
+            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly,
+                    seqNum);
             if (!skipCache && isCacheable(type)) {
                 mPreparedStatementCache.put(sql, statement);
                 statement.mInCache = true;
@@ -1112,15 +1120,12 @@
      * Return a {@link #PreparedStatement}, possibly from the cache.
      */
     PreparedStatement acquirePreparedStatement(String sql) {
-        synchronized (mCacheLock) {
-            return acquirePreparedStatementLI(sql);
-        }
+        return acquirePreparedStatementLI(sql);
     }
 
     /**
      * Release a {@link #PreparedStatement} that was originally supplied by this connection.
      */
-    @GuardedBy("mCacheLock")
     private void releasePreparedStatementLI(PreparedStatement statement) {
         statement.mInUse = false;
         if (statement.mInCache) {
@@ -1148,9 +1153,7 @@
      * Release a {@link #PreparedStatement} that was originally supplied by this connection.
      */
     void releasePreparedStatement(PreparedStatement statement) {
-        synchronized (mCacheLock) {
-            releasePreparedStatementLI(statement);
-        }
+        releasePreparedStatementLI(statement);
     }
 
     private void finalizePreparedStatement(PreparedStatement statement) {
@@ -1327,9 +1330,7 @@
         mRecentOperations.dump(printer);
 
         if (verbose) {
-            synchronized (mCacheLock) {
-                mPreparedStatementCache.dump(printer);
-            }
+            mPreparedStatementCache.dump(printer);
         }
     }
 
@@ -1430,7 +1431,7 @@
     }
 
     private PreparedStatement obtainPreparedStatement(String sql, long statementPtr,
-            int numParameters, int type, boolean readOnly) {
+            int numParameters, int type, boolean readOnly, long seqNum) {
         PreparedStatement statement = mPreparedStatementPool;
         if (statement != null) {
             mPreparedStatementPool = statement.mPoolNext;
@@ -1444,6 +1445,7 @@
         statement.mNumParameters = numParameters;
         statement.mType = type;
         statement.mReadOnly = readOnly;
+        statement.mSeqNum = seqNum;
         return statement;
     }
 
@@ -1461,10 +1463,10 @@
         return sql.replaceAll("[\\s]*\\n+[\\s]*", " ");
     }
 
-    void clearPreparedStatementCache() {
-        synchronized (mCacheLock) {
-            mPreparedStatementCache.evictAll();
-        }
+    // Update the database sequence number.  This number is stored in the prepared statement
+    // cache.
+    void setDatabaseSeqNum(long n) {
+        mPreparedStatementCache.setDatabaseSeqNum(n);
     }
 
     /**
@@ -1502,6 +1504,10 @@
         // True if the statement is in the cache.
         public boolean mInCache;
 
+        // The database schema ID at the time this statement was created.  The ID is left zero for
+        // statements that are not cached.  This value is meaningful only if mInCache is true.
+        public long mSeqNum;
+
         // True if the statement is in use (currently executing).
         // We need this flag because due to the use of custom functions in triggers, it's
         // possible for SQLite calls to be re-entrant.  Consequently we need to prevent
@@ -1510,10 +1516,41 @@
     }
 
     private final class PreparedStatementCache extends LruCache<String, PreparedStatement> {
+        // The database sequence number.  This changes every time the database schema changes.
+        private long mDatabaseSeqNum = 0;
+
+        // The database sequence number from the last getStatement() or createStatement()
+        // call. The proper use of this variable depends on the caller being single threaded.
+        private long mLastSeqNum = 0;
+
         public PreparedStatementCache(int size) {
             super(size);
         }
 
+        public synchronized void setDatabaseSeqNum(long n) {
+            mDatabaseSeqNum = n;
+        }
+
+        // Return the last database sequence number.
+        public long getLastSeqNum() {
+            return mLastSeqNum;
+        }
+
+        // Return a statement from the cache.  Save the database sequence number for the caller.
+        public synchronized PreparedStatement getStatement(String sql) {
+            mLastSeqNum = mDatabaseSeqNum;
+            return get(sql);
+        }
+
+        // Return a new native prepared statement and save the database sequence number for the
+        // caller.  This does not modify the cache in any way.  However, by being synchronized,
+        // callers are guaranteed that the sequence number did not change across the native
+        // preparation step.
+        public synchronized long createStatement(String sql) {
+            mLastSeqNum = mDatabaseSeqNum;
+            return nativePrepareStatement(mConnectionPtr, sql);
+        }
+
         @Override
         protected void entryRemoved(boolean evicted, String key,
                 PreparedStatement oldValue, PreparedStatement newValue) {
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index b35a2e4..ad335b6 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -111,6 +111,13 @@
     @GuardedBy("mLock")
     private IdleConnectionHandler mIdleConnectionHandler;
 
+    // The database schema sequence number.  This counter is incremented every time a schema
+    // change is detected.  Every prepared statement records its schema sequence when the
+    // statement is created.  The prepared statement is not put back in the cache if the sequence
+    // number has changed.  The counter starts at 1, which allows clients to use 0 as a
+    // distinguished value.
+    private long mDatabaseSeqNum = 1;
+
     // whole execution time for this connection in milliseconds.
     private final AtomicLong mTotalStatementsTime = new AtomicLong(0);
 
@@ -1127,10 +1134,12 @@
     }
 
     void clearAcquiredConnectionsPreparedStatementCache() {
+        // Invalidate prepared statements that have an earlier schema sequence number.
         synchronized (mLock) {
+            mDatabaseSeqNum++;
             if (!mAcquiredConnections.isEmpty()) {
                 for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
-                    connection.clearPreparedStatementCache();
+                    connection.setDatabaseSeqNum(mDatabaseSeqNum);
                 }
             }
         }
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 44fed67..60b11b4 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -2986,8 +2986,6 @@
         ImeTracing.getInstance().triggerServiceDump(
                 "InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
                 null /* icProto */);
-        ImeTracker.forLogging().onProgress(mCurStatsToken,
-                ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
         mPrivOps.applyImeVisibilityAsync(setVisible
                 ? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
     }
diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java
index b74bb33..82cdd28 100644
--- a/core/java/android/os/LocaleList.java
+++ b/core/java/android/os/LocaleList.java
@@ -30,6 +30,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -151,6 +152,25 @@
     }
 
     /**
+     * Find the intersection between this LocaleList and another
+     * @return a String array of the Locales in both LocaleLists
+     * {@hide}
+     */
+    @NonNull
+    public String[] getIntersection(@NonNull LocaleList other) {
+        List<String> intersection = new ArrayList<>();
+        for (Locale l1 : mList) {
+            for (Locale l2 : other.mList) {
+                if (matchesLanguageAndScript(l2, l1)) {
+                    intersection.add(l1.toLanguageTag());
+                    break;
+                }
+            }
+        }
+        return intersection.toArray(new String[0]);
+    }
+
+    /**
      * Creates a new {@link LocaleList}.
      *
      * If two or more same locales are passed, the repeated locales will be dropped.
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index ea5499f..954ee3c 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -14,6 +14,19 @@
       ]
     },
     {
+      "file_patterns": [
+        "[^/]*(Vibrator|Vibration)[^/]*\\.java",
+        "vibrator/.*"
+      ],
+      "name": "FrameworksVibratorServicesTests",
+      "options": [
+        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    },
+    {
       "file_patterns": ["Bugreport[^/]*\\.java"],
       "name": "BugreportManagerTestCases",
       "options": [
diff --git a/core/java/android/view/InputEventSender.java b/core/java/android/view/InputEventSender.java
index 64f62c7..c843bbe 100644
--- a/core/java/android/view/InputEventSender.java
+++ b/core/java/android/view/InputEventSender.java
@@ -18,6 +18,7 @@
 
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.MessageQueue;
 import android.util.Log;
@@ -25,6 +26,10 @@
 import dalvik.system.CloseGuard;
 
 import java.lang.ref.WeakReference;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RunnableFuture;
 
 /**
  * Provides a low-level mechanism for an application to send input events.
@@ -37,10 +42,10 @@
 
     private long mSenderPtr;
 
-    // We keep references to the input channel and message queue objects here so that
-    // they are not GC'd while the native peer of the receiver is using them.
+    // We keep references to the input channel and message queue objects (indirectly through
+    // Handler) here so that they are not GC'd while the native peer of the receiver is using them.
     private InputChannel mInputChannel;
-    private MessageQueue mMessageQueue;
+    private Handler mHandler;
 
     private static native long nativeInit(WeakReference<InputEventSender> sender,
             InputChannel inputChannel, MessageQueue messageQueue);
@@ -63,9 +68,9 @@
         }
 
         mInputChannel = inputChannel;
-        mMessageQueue = looper.getQueue();
+        mHandler = new Handler(looper);
         mSenderPtr = nativeInit(new WeakReference<InputEventSender>(this),
-                mInputChannel, mMessageQueue);
+                mInputChannel, looper.getQueue());
 
         mCloseGuard.open("InputEventSender.dispose");
     }
@@ -98,8 +103,8 @@
             nativeDispose(mSenderPtr);
             mSenderPtr = 0;
         }
+        mHandler = null;
         mInputChannel = null;
-        mMessageQueue = null;
     }
 
     /**
@@ -122,8 +127,8 @@
     }
 
     /**
-     * Sends an input event.
-     * Must be called on the same Looper thread to which the sender is attached.
+     * Sends an input event. Can be called from any thread. Do not call this if the looper thread
+     * is blocked! It would cause a deadlock.
      *
      * @param seq The input event sequence number.
      * @param event The input event to send.
@@ -140,6 +145,28 @@
             return false;
         }
 
+        if (mHandler.getLooper().isCurrentThread()) {
+            return sendInputEventInternal(seq, event);
+        }
+        // This is being called on another thread. Post a runnable to the looper thread
+        // with the event injection, and wait until it's processed.
+        final RunnableFuture<Boolean> task = new FutureTask<>(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                return sendInputEventInternal(seq, event);
+            }
+        });
+        mHandler.post(task);
+        try {
+            return task.get();
+        } catch (InterruptedException exc) {
+            throw new IllegalStateException("Interrupted while sending " + event + ": " + exc);
+        } catch (ExecutionException exc) {
+            throw new IllegalStateException("Couldn't send " + event + ": " + exc);
+        }
+    }
+
+    private boolean sendInputEventInternal(int seq, InputEvent event) {
         if (event instanceof KeyEvent) {
             return nativeSendKeyEvent(mSenderPtr, seq, (KeyEvent)event);
         } else {
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index e153bb7..43fa0be 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -80,6 +80,14 @@
      */
     public static final int OP_TYPE_REORDER_TO_FRONT = 10;
 
+    /**
+     * Sets the activity navigation to be isolated, where the activity navigation on the
+     * TaskFragment is separated from the rest activities in the Task. Activities cannot be
+     * started on an isolated TaskFragment unless the activities are launched from the same
+     * TaskFragment or explicitly requested to.
+     */
+    public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11;
+
     @IntDef(prefix = { "OP_TYPE_" }, value = {
             OP_TYPE_UNKNOWN,
             OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -92,7 +100,8 @@
             OP_TYPE_SET_COMPANION_TASK_FRAGMENT,
             OP_TYPE_SET_ANIMATION_PARAMS,
             OP_TYPE_SET_RELATIVE_BOUNDS,
-            OP_TYPE_REORDER_TO_FRONT
+            OP_TYPE_REORDER_TO_FRONT,
+            OP_TYPE_SET_ISOLATED_NAVIGATION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface OperationType {}
@@ -118,11 +127,14 @@
     @Nullable
     private final TaskFragmentAnimationParams mAnimationParams;
 
+    private final boolean mIsolatedNav;
+
     private TaskFragmentOperation(@OperationType int opType,
             @Nullable TaskFragmentCreationParams taskFragmentCreationParams,
             @Nullable IBinder activityToken, @Nullable Intent activityIntent,
             @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
-            @Nullable TaskFragmentAnimationParams animationParams) {
+            @Nullable TaskFragmentAnimationParams animationParams,
+            boolean isolatedNav) {
         mOpType = opType;
         mTaskFragmentCreationParams = taskFragmentCreationParams;
         mActivityToken = activityToken;
@@ -130,6 +142,7 @@
         mBundle = bundle;
         mSecondaryFragmentToken = secondaryFragmentToken;
         mAnimationParams = animationParams;
+        mIsolatedNav = isolatedNav;
     }
 
     private TaskFragmentOperation(Parcel in) {
@@ -140,6 +153,7 @@
         mBundle = in.readBundle(getClass().getClassLoader());
         mSecondaryFragmentToken = in.readStrongBinder();
         mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
+        mIsolatedNav = in.readBoolean();
     }
 
     @Override
@@ -151,6 +165,7 @@
         dest.writeBundle(mBundle);
         dest.writeStrongBinder(mSecondaryFragmentToken);
         dest.writeTypedObject(mAnimationParams, flags);
+        dest.writeBoolean(mIsolatedNav);
     }
 
     @NonNull
@@ -223,6 +238,14 @@
         return mAnimationParams;
     }
 
+    /**
+     * Returns whether the activity navigation on this TaskFragment is isolated. This is only
+     * useful when the op type is {@link OP_TYPE_SET_ISOLATED_NAVIGATION}.
+     */
+    public boolean isIsolatedNav() {
+        return mIsolatedNav;
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder();
@@ -245,6 +268,7 @@
         if (mAnimationParams != null) {
             sb.append(", animationParams=").append(mAnimationParams);
         }
+        sb.append(", isolatedNav=").append(mIsolatedNav);
 
         sb.append('}');
         return sb.toString();
@@ -253,7 +277,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
-                mBundle, mSecondaryFragmentToken, mAnimationParams);
+                mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav);
     }
 
     @Override
@@ -268,7 +292,8 @@
                 && Objects.equals(mActivityIntent, other.mActivityIntent)
                 && Objects.equals(mBundle, other.mBundle)
                 && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
-                && Objects.equals(mAnimationParams, other.mAnimationParams);
+                && Objects.equals(mAnimationParams, other.mAnimationParams)
+                && mIsolatedNav == other.mIsolatedNav;
     }
 
     @Override
@@ -300,6 +325,8 @@
         @Nullable
         private TaskFragmentAnimationParams mAnimationParams;
 
+        private boolean mIsolatedNav;
+
         /**
          * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
          */
@@ -363,12 +390,22 @@
         }
 
         /**
+         * Sets the activity navigation of this TaskFragment to be isolated.
+         */
+        @NonNull
+        public Builder setIsolatedNav(boolean isolatedNav) {
+            mIsolatedNav = isolatedNav;
+            return this;
+        }
+
+        /**
          * Constructs the {@link TaskFragmentOperation}.
          */
         @NonNull
         public TaskFragmentOperation build() {
             return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
-                    mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams);
+                    mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
+                    mIsolatedNav);
         }
     }
 }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 8a5c7ef..30ebbe2 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -386,8 +386,12 @@
             @Nullable ImeTracker.Token statsToken) {
         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
         if (ops == null) {
+            ImeTracker.forLogging().onFailed(statsToken,
+                    ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
             return;
         }
+        ImeTracker.forLogging().onProgress(statsToken,
+                ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
         try {
             ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
         } catch (RemoteException e) {
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 979c9e3..1afae29 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -347,14 +347,23 @@
 }
 
 static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc,
-                                   jstring locale, jint orientation, jint touchscreen, jint density,
-                                   jint keyboard, jint keyboard_hidden, jint navigation,
-                                   jint screen_width, jint screen_height,
-                                   jint smallest_screen_width_dp, jint screen_width_dp,
-                                   jint screen_height_dp, jint screen_layout, jint ui_mode,
-                                   jint color_mode, jint grammatical_gender, jint major_version) {
+                                   jstring default_locale, jobjectArray locales, jint orientation,
+                                   jint touchscreen, jint density, jint keyboard,
+                                   jint keyboard_hidden, jint navigation, jint screen_width,
+                                   jint screen_height, jint smallest_screen_width_dp,
+                                   jint screen_width_dp, jint screen_height_dp, jint screen_layout,
+                                   jint ui_mode, jint color_mode, jint grammatical_gender,
+                                   jint major_version) {
   ATRACE_NAME("AssetManager::SetConfiguration");
 
+  const jsize locale_count = (locales == NULL) ? 0 : env->GetArrayLength(locales);
+
+  // Constants duplicated from Java class android.content.res.Configuration.
+  static const jint kScreenLayoutRoundMask = 0x300;
+  static const jint kScreenLayoutRoundShift = 8;
+
+  std::vector<ResTable_config> configs;
+
   ResTable_config configuration;
   memset(&configuration, 0, sizeof(configuration));
   configuration.mcc = static_cast<uint16_t>(mcc);
@@ -375,25 +384,37 @@
   configuration.colorMode = static_cast<uint8_t>(color_mode);
   configuration.grammaticalInflection = static_cast<uint8_t>(grammatical_gender);
   configuration.sdkVersion = static_cast<uint16_t>(major_version);
-
-  if (locale != nullptr) {
-    ScopedUtfChars locale_utf8(env, locale);
-    CHECK(locale_utf8.c_str() != nullptr);
-    configuration.setBcp47Locale(locale_utf8.c_str());
-  }
-
-  // Constants duplicated from Java class android.content.res.Configuration.
-  static const jint kScreenLayoutRoundMask = 0x300;
-  static const jint kScreenLayoutRoundShift = 8;
-
   // In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer
   // in C++. We must extract the round qualifier out of the Java screenLayout and put it
   // into screenLayout2.
   configuration.screenLayout2 =
-      static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift);
+          static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift);
+
+  if (locale_count > 0) {
+    configs.resize(locale_count, configuration);
+    for (int i = 0; i < locale_count; i++) {
+      jstring locale = (jstring)(env->GetObjectArrayElement(locales, i));
+      ScopedUtfChars locale_utf8(env, locale);
+      CHECK(locale_utf8.c_str() != nullptr);
+      configs[i].setBcp47Locale(locale_utf8.c_str());
+    }
+  } else {
+    configs.push_back(configuration);
+  }
+
+  uint32_t default_locale_int = 0;
+  if (default_locale != nullptr) {
+    ResTable_config config;
+    static_assert(std::is_same_v<decltype(config.locale), decltype(default_locale_int)>);
+    ScopedUtfChars locale_utf8(env, default_locale);
+    CHECK(locale_utf8.c_str() != nullptr);
+    config.setBcp47Locale(locale_utf8.c_str());
+    default_locale_int = config.locale;
+  }
 
   auto assetmanager = LockAndStartAssetManager(ptr);
-  assetmanager->SetConfiguration(configuration);
+  assetmanager->SetConfigurations(configs);
+  assetmanager->SetDefaultLocale(default_locale_int);
 }
 
 static jobject NativeGetAssignedPackageIdentifiers(JNIEnv* env, jclass /*clazz*/, jlong ptr,
@@ -1498,94 +1519,97 @@
 
 // JNI registration.
 static const JNINativeMethod gAssetManagerMethods[] = {
-    // AssetManager setup methods.
-    {"nativeCreate", "()J", (void*)NativeCreate},
-    {"nativeDestroy", "(J)V", (void*)NativeDestroy},
-    {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets},
-    {"nativeSetConfiguration", "(JIILjava/lang/String;IIIIIIIIIIIIIIII)V",
-     (void*)NativeSetConfiguration},
-    {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;",
-     (void*)NativeGetAssignedPackageIdentifiers},
+        // AssetManager setup methods.
+        {"nativeCreate", "()J", (void*)NativeCreate},
+        {"nativeDestroy", "(J)V", (void*)NativeDestroy},
+        {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets},
+        {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIII)V",
+         (void*)NativeSetConfiguration},
+        {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;",
+         (void*)NativeGetAssignedPackageIdentifiers},
 
-    // AssetManager file methods.
-    {"nativeContainsAllocatedTable", "(J)Z", (void*)ContainsAllocatedTable},
-    {"nativeList", "(JLjava/lang/String;)[Ljava/lang/String;", (void*)NativeList},
-    {"nativeOpenAsset", "(JLjava/lang/String;I)J", (void*)NativeOpenAsset},
-    {"nativeOpenAssetFd", "(JLjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;",
-     (void*)NativeOpenAssetFd},
-    {"nativeOpenNonAsset", "(JILjava/lang/String;I)J", (void*)NativeOpenNonAsset},
-    {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;",
-     (void*)NativeOpenNonAssetFd},
-    {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset},
-    {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd},
+        // AssetManager file methods.
+        {"nativeContainsAllocatedTable", "(J)Z", (void*)ContainsAllocatedTable},
+        {"nativeList", "(JLjava/lang/String;)[Ljava/lang/String;", (void*)NativeList},
+        {"nativeOpenAsset", "(JLjava/lang/String;I)J", (void*)NativeOpenAsset},
+        {"nativeOpenAssetFd", "(JLjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;",
+         (void*)NativeOpenAssetFd},
+        {"nativeOpenNonAsset", "(JILjava/lang/String;I)J", (void*)NativeOpenNonAsset},
+        {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;",
+         (void*)NativeOpenNonAssetFd},
+        {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset},
+        {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd},
 
-    // AssetManager resource methods.
-    {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I", (void*)NativeGetResourceValue},
-    {"nativeGetResourceBagValue", "(JIILandroid/util/TypedValue;)I",
-     (void*)NativeGetResourceBagValue},
-    {"nativeGetStyleAttributes", "(JI)[I", (void*)NativeGetStyleAttributes},
-    {"nativeGetResourceStringArray", "(JI)[Ljava/lang/String;",
-     (void*)NativeGetResourceStringArray},
-    {"nativeGetResourceStringArrayInfo", "(JI)[I", (void*)NativeGetResourceStringArrayInfo},
-    {"nativeGetResourceIntArray", "(JI)[I", (void*)NativeGetResourceIntArray},
-    {"nativeGetResourceArraySize", "(JI)I", (void*)NativeGetResourceArraySize},
-    {"nativeGetResourceArray", "(JI[I)I", (void*)NativeGetResourceArray},
-    {"nativeGetParentThemeIdentifier", "(JI)I",
-     (void*)NativeGetParentThemeIdentifier},
+        // AssetManager resource methods.
+        {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I",
+         (void*)NativeGetResourceValue},
+        {"nativeGetResourceBagValue", "(JIILandroid/util/TypedValue;)I",
+         (void*)NativeGetResourceBagValue},
+        {"nativeGetStyleAttributes", "(JI)[I", (void*)NativeGetStyleAttributes},
+        {"nativeGetResourceStringArray", "(JI)[Ljava/lang/String;",
+         (void*)NativeGetResourceStringArray},
+        {"nativeGetResourceStringArrayInfo", "(JI)[I", (void*)NativeGetResourceStringArrayInfo},
+        {"nativeGetResourceIntArray", "(JI)[I", (void*)NativeGetResourceIntArray},
+        {"nativeGetResourceArraySize", "(JI)I", (void*)NativeGetResourceArraySize},
+        {"nativeGetResourceArray", "(JI[I)I", (void*)NativeGetResourceArray},
+        {"nativeGetParentThemeIdentifier", "(JI)I", (void*)NativeGetParentThemeIdentifier},
 
-    // AssetManager resource name/ID methods.
-    {"nativeGetResourceIdentifier", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
-     (void*)NativeGetResourceIdentifier},
-    {"nativeGetResourceName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceName},
-    {"nativeGetResourcePackageName", "(JI)Ljava/lang/String;", (void*)NativeGetResourcePackageName},
-    {"nativeGetResourceTypeName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceTypeName},
-    {"nativeGetResourceEntryName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceEntryName},
-    {"nativeSetResourceResolutionLoggingEnabled", "(JZ)V",
-     (void*) NativeSetResourceResolutionLoggingEnabled},
-    {"nativeGetLastResourceResolution", "(J)Ljava/lang/String;",
-     (void*) NativeGetLastResourceResolution},
-    {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales},
-    {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;",
-     (void*)NativeGetSizeConfigurations},
-    {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;",
-     (void*)NativeGetSizeAndUiModeConfigurations},
+        // AssetManager resource name/ID methods.
+        {"nativeGetResourceIdentifier",
+         "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
+         (void*)NativeGetResourceIdentifier},
+        {"nativeGetResourceName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceName},
+        {"nativeGetResourcePackageName", "(JI)Ljava/lang/String;",
+         (void*)NativeGetResourcePackageName},
+        {"nativeGetResourceTypeName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceTypeName},
+        {"nativeGetResourceEntryName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceEntryName},
+        {"nativeSetResourceResolutionLoggingEnabled", "(JZ)V",
+         (void*)NativeSetResourceResolutionLoggingEnabled},
+        {"nativeGetLastResourceResolution", "(J)Ljava/lang/String;",
+         (void*)NativeGetLastResourceResolution},
+        {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales},
+        {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;",
+         (void*)NativeGetSizeConfigurations},
+        {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;",
+         (void*)NativeGetSizeAndUiModeConfigurations},
 
-    // Style attribute related methods.
-    {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack},
-    {"nativeApplyStyle", "(JJIIJ[IJJ)V", (void*)NativeApplyStyle},
-    {"nativeResolveAttrs", "(JJII[I[I[I[I)Z", (void*)NativeResolveAttrs},
-    {"nativeRetrieveAttributes", "(JJ[I[I[I)Z", (void*)NativeRetrieveAttributes},
+        // Style attribute related methods.
+        {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack},
+        {"nativeApplyStyle", "(JJIIJ[IJJ)V", (void*)NativeApplyStyle},
+        {"nativeResolveAttrs", "(JJII[I[I[I[I)Z", (void*)NativeResolveAttrs},
+        {"nativeRetrieveAttributes", "(JJ[I[I[I)Z", (void*)NativeRetrieveAttributes},
 
-    // Theme related methods.
-    {"nativeThemeCreate", "(J)J", (void*)NativeThemeCreate},
-    {"nativeGetThemeFreeFunction", "()J", (void*)NativeGetThemeFreeFunction},
-    {"nativeThemeApplyStyle", "(JJIZ)V", (void*)NativeThemeApplyStyle},
-    {"nativeThemeRebase", "(JJ[I[ZI)V", (void*)NativeThemeRebase},
+        // Theme related methods.
+        {"nativeThemeCreate", "(J)J", (void*)NativeThemeCreate},
+        {"nativeGetThemeFreeFunction", "()J", (void*)NativeGetThemeFreeFunction},
+        {"nativeThemeApplyStyle", "(JJIZ)V", (void*)NativeThemeApplyStyle},
+        {"nativeThemeRebase", "(JJ[I[ZI)V", (void*)NativeThemeRebase},
 
-    {"nativeThemeCopy", "(JJJJ)V", (void*)NativeThemeCopy},
-    {"nativeThemeGetAttributeValue", "(JJILandroid/util/TypedValue;Z)I",
-     (void*)NativeThemeGetAttributeValue},
-    {"nativeThemeDump", "(JJILjava/lang/String;Ljava/lang/String;)V", (void*)NativeThemeDump},
-    {"nativeThemeGetChangingConfigurations", "(J)I", (void*)NativeThemeGetChangingConfigurations},
+        {"nativeThemeCopy", "(JJJJ)V", (void*)NativeThemeCopy},
+        {"nativeThemeGetAttributeValue", "(JJILandroid/util/TypedValue;Z)I",
+         (void*)NativeThemeGetAttributeValue},
+        {"nativeThemeDump", "(JJILjava/lang/String;Ljava/lang/String;)V", (void*)NativeThemeDump},
+        {"nativeThemeGetChangingConfigurations", "(J)I",
+         (void*)NativeThemeGetChangingConfigurations},
 
-    // AssetInputStream methods.
-    {"nativeAssetDestroy", "(J)V", (void*)NativeAssetDestroy},
-    {"nativeAssetReadChar", "(J)I", (void*)NativeAssetReadChar},
-    {"nativeAssetRead", "(J[BII)I", (void*)NativeAssetRead},
-    {"nativeAssetSeek", "(JJI)J", (void*)NativeAssetSeek},
-    {"nativeAssetGetLength", "(J)J", (void*)NativeAssetGetLength},
-    {"nativeAssetGetRemainingLength", "(J)J", (void*)NativeAssetGetRemainingLength},
+        // AssetInputStream methods.
+        {"nativeAssetDestroy", "(J)V", (void*)NativeAssetDestroy},
+        {"nativeAssetReadChar", "(J)I", (void*)NativeAssetReadChar},
+        {"nativeAssetRead", "(J[BII)I", (void*)NativeAssetRead},
+        {"nativeAssetSeek", "(JJI)J", (void*)NativeAssetSeek},
+        {"nativeAssetGetLength", "(J)J", (void*)NativeAssetGetLength},
+        {"nativeAssetGetRemainingLength", "(J)J", (void*)NativeAssetGetRemainingLength},
 
-    // System/idmap related methods.
-    {"nativeGetOverlayableMap", "(JLjava/lang/String;)Ljava/util/Map;",
-     (void*)NativeGetOverlayableMap},
-    {"nativeGetOverlayablesToString", "(JLjava/lang/String;)Ljava/lang/String;",
-     (void*)NativeGetOverlayablesToString},
+        // System/idmap related methods.
+        {"nativeGetOverlayableMap", "(JLjava/lang/String;)Ljava/util/Map;",
+         (void*)NativeGetOverlayableMap},
+        {"nativeGetOverlayablesToString", "(JLjava/lang/String;)Ljava/lang/String;",
+         (void*)NativeGetOverlayablesToString},
 
-    // Global management/debug methods.
-    {"getGlobalAssetCount", "()I", (void*)NativeGetGlobalAssetCount},
-    {"getAssetAllocations", "()Ljava/lang/String;", (void*)NativeGetAssetAllocations},
-    {"getGlobalAssetManagerCount", "()I", (void*)NativeGetGlobalAssetManagerCount},
+        // Global management/debug methods.
+        {"getGlobalAssetCount", "()I", (void*)NativeGetGlobalAssetCount},
+        {"getAssetAllocations", "()Ljava/lang/String;", (void*)NativeGetAssetAllocations},
+        {"getGlobalAssetManagerCount", "()I", (void*)NativeGetGlobalAssetManagerCount},
 };
 
 int register_android_content_AssetManager(JNIEnv* env) {
diff --git a/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml b/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml
new file mode 100644
index 0000000..bd77e59
--- /dev/null
+++ b/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml
@@ -0,0 +1,944 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 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.
+-->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="983"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0.1"
+                    android:valueTo="0.1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="983"
+                    android:valueFrom="0.1"
+                    android:valueTo="0.1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="983"
+                    android:pathData="M 50,50C 50,50 50,50 50,50"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="0">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:pathData="M 50,50C 50,50 50,50 50,50"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="983">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_1_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="983"
+                    android:pathData="M 32,12C 32,12 32,12 32,12"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="0">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:pathData="M 32,12C 32,12 32,12 32,12"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="983">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="83"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.999,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="400"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="583"
+                    android:valueFrom="1"
+                    android:valueTo="0.0008500000000000001"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.201,0.967 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="983"
+                    android:valueFrom="0.0008500000000000001"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.329,0.663 0.662,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-2053 -413 C-2064.2,-412.04 -2074.66,-410.29 -2084.48,-407.87 C-2094.29,-405.46 -2103.47,-402.36 -2112.09,-398.71 C-2120.71,-395.06 -2128.79,-390.85 -2136.41,-386.19 C-2144.02,-381.54 -2151.19,-376.43 -2158,-371 C-2168.19,-362.86 -2177.05,-354.51 -2184.8,-345.79 C-2192.55,-337.07 -2199.2,-327.99 -2204.98,-318.41 C-2210.77,-308.83 -2215.7,-298.74 -2220,-288 C-2237.22,-245.04 -2237.06,-202.14 -2226.93,-164.3 C-2216.8,-126.46 -2196.69,-93.69 -2174,-71 C-2150.2,-47.2 -2115.92,-28.38 -2077.59,-19.79 C-2039.27,-11.2 -1996.92,-12.85 -1957,-30 C-1925.35,-43.59 -1896.01,-64.58 -1873.88,-92.68 C-1851.74,-120.77 -1836.82,-155.98 -1834,-198 C-1831.53,-234.86 -1837.92,-266 -1849.1,-292.1 C-1860.29,-318.21 -1876.28,-339.28 -1893,-356 C-1910.36,-373.36 -1932.3,-389.15 -1958.94,-399.84 C-1985.57,-410.52 -2016.89,-416.09 -2053,-413c "
+                    android:valueTo="M-2053 -413 C-2072.13,-411.36 -2090.2,-406.86 -2106.82,-400.38 C-2123.44,-393.91 -2138.61,-385.46 -2151.93,-375.94 C-2165.26,-366.41 -2176.74,-355.8 -2186,-345 C-2200.2,-328.44 -2213.75,-307.38 -2222.86,-282.11 C-2231.96,-256.85 -2236.61,-227.38 -2233,-194 C-2229.85,-164.84 -2221.85,-140.07 -2210.02,-118.6 C-2198.19,-97.13 -2182.51,-78.96 -2164,-63 C-2146.03,-47.52 -2123.67,-34.03 -2098.23,-25.16 C-2072.79,-16.29 -2044.27,-12.03 -2014,-15 C-1985.05,-17.84 -1959.94,-25.74 -1938.13,-37.35 C-1916.32,-48.97 -1897.79,-64.3 -1882,-82 C-1866.83,-98.99 -1853.27,-120.85 -1844.28,-146.28 C-1835.29,-171.72 -1830.87,-200.72 -1834,-232 C-1836.98,-261.8 -1845.16,-287.41 -1856.88,-309.38 C-1868.6,-331.36 -1883.86,-349.71 -1901,-365 C-1917.51,-379.74 -1939.59,-393.1 -1965.5,-402.1 C-1991.42,-411.09 -2021.16,-415.73 -2053,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="17"
+                    android:valueFrom="M-2053 -413 C-2072.13,-411.36 -2090.2,-406.86 -2106.82,-400.38 C-2123.44,-393.91 -2138.61,-385.46 -2151.93,-375.94 C-2165.26,-366.41 -2176.74,-355.8 -2186,-345 C-2200.2,-328.44 -2213.75,-307.38 -2222.86,-282.11 C-2231.96,-256.85 -2236.61,-227.38 -2233,-194 C-2229.85,-164.84 -2221.85,-140.07 -2210.02,-118.6 C-2198.19,-97.13 -2182.51,-78.96 -2164,-63 C-2146.03,-47.52 -2123.67,-34.03 -2098.23,-25.16 C-2072.79,-16.29 -2044.27,-12.03 -2014,-15 C-1985.05,-17.84 -1959.94,-25.74 -1938.13,-37.35 C-1916.32,-48.97 -1897.79,-64.3 -1882,-82 C-1866.83,-98.99 -1853.27,-120.85 -1844.28,-146.28 C-1835.29,-171.72 -1830.87,-200.72 -1834,-232 C-1836.98,-261.8 -1845.16,-287.41 -1856.88,-309.38 C-1868.6,-331.36 -1883.86,-349.71 -1901,-365 C-1917.51,-379.74 -1939.59,-393.1 -1965.5,-402.1 C-1991.42,-411.09 -2021.16,-415.73 -2053,-413c "
+                    android:valueTo="M-2052 -413 C-2071.28,-411.42 -2089.49,-406.96 -2106.24,-400.51 C-2122.98,-394.05 -2138.26,-385.61 -2151.69,-376.07 C-2165.12,-366.52 -2176.68,-355.87 -2186,-345 C-2200.23,-328.39 -2213.89,-307.21 -2223.04,-281.77 C-2232.19,-256.33 -2236.82,-226.63 -2233,-193 C-2229.75,-164.4 -2221.68,-139.52 -2209.83,-117.85 C-2197.98,-96.18 -2182.35,-77.73 -2164,-62 C-2146.19,-46.74 -2123.18,-33.34 -2097.25,-24.47 C-2071.32,-15.59 -2042.48,-11.22 -2013,-14 C-1983.8,-16.75 -1958.61,-24.73 -1936.82,-36.53 C-1915.02,-48.33 -1896.62,-63.95 -1881,-82 C-1865.71,-99.67 -1851.88,-121.33 -1842.66,-146.49 C-1833.45,-171.65 -1828.84,-200.32 -1832,-232 C-1835.01,-262.26 -1843.42,-287.88 -1855.37,-309.62 C-1867.32,-331.36 -1882.81,-349.22 -1900,-364 C-1917.39,-378.94 -1938.96,-392.54 -1964.39,-401.73 C-1989.83,-410.92 -2019.14,-415.7 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="33"
+                    android:valueFrom="M-2052 -413 C-2071.28,-411.42 -2089.49,-406.96 -2106.24,-400.51 C-2122.98,-394.05 -2138.26,-385.61 -2151.69,-376.07 C-2165.12,-366.52 -2176.68,-355.87 -2186,-345 C-2200.23,-328.39 -2213.89,-307.21 -2223.04,-281.77 C-2232.19,-256.33 -2236.82,-226.63 -2233,-193 C-2229.75,-164.4 -2221.68,-139.52 -2209.83,-117.85 C-2197.98,-96.18 -2182.35,-77.73 -2164,-62 C-2146.19,-46.74 -2123.18,-33.34 -2097.25,-24.47 C-2071.32,-15.59 -2042.48,-11.22 -2013,-14 C-1983.8,-16.75 -1958.61,-24.73 -1936.82,-36.53 C-1915.02,-48.33 -1896.62,-63.95 -1881,-82 C-1865.71,-99.67 -1851.88,-121.33 -1842.66,-146.49 C-1833.45,-171.65 -1828.84,-200.32 -1832,-232 C-1835.01,-262.26 -1843.42,-287.88 -1855.37,-309.62 C-1867.32,-331.36 -1882.81,-349.22 -1900,-364 C-1917.39,-378.94 -1938.96,-392.54 -1964.39,-401.73 C-1989.83,-410.92 -2019.14,-415.7 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2071.66,-411.39 -2090,-406.88 -2106.76,-400.35 C-2123.51,-393.82 -2138.67,-385.28 -2151.97,-375.59 C-2165.26,-365.91 -2176.7,-355.08 -2186,-344 C-2200.29,-326.96 -2214.12,-305.92 -2223.33,-280.54 C-2232.55,-255.16 -2237.15,-225.42 -2233,-191 C-2229.56,-162.51 -2221.15,-137.78 -2208.93,-116.29 C-2196.71,-94.8 -2180.68,-76.54 -2162,-61 C-2143.85,-45.9 -2120.72,-32.5 -2094.69,-23.57 C-2068.65,-14.65 -2039.73,-10.19 -2010,-13 C-1981.12,-15.72 -1955.63,-23.67 -1933.48,-35.47 C-1911.33,-47.26 -1892.52,-62.9 -1877,-81 C-1861.92,-98.59 -1848.2,-120.71 -1839.11,-146.38 C-1830.02,-172.04 -1825.57,-201.24 -1829,-233 C-1832.23,-262.84 -1840.89,-288.53 -1853.04,-310.41 C-1865.19,-332.3 -1880.82,-350.37 -1898,-365 C-1915.02,-379.5 -1937.52,-392.81 -1963.76,-401.84 C-1990.01,-410.88 -2020,-415.63 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="50"
+                    android:valueFrom="M-2052 -413 C-2071.66,-411.39 -2090,-406.88 -2106.76,-400.35 C-2123.51,-393.82 -2138.67,-385.28 -2151.97,-375.59 C-2165.26,-365.91 -2176.7,-355.08 -2186,-344 C-2200.29,-326.96 -2214.12,-305.92 -2223.33,-280.54 C-2232.55,-255.16 -2237.15,-225.42 -2233,-191 C-2229.56,-162.51 -2221.15,-137.78 -2208.93,-116.29 C-2196.71,-94.8 -2180.68,-76.54 -2162,-61 C-2143.85,-45.9 -2120.72,-32.5 -2094.69,-23.57 C-2068.65,-14.65 -2039.73,-10.19 -2010,-13 C-1981.12,-15.72 -1955.63,-23.67 -1933.48,-35.47 C-1911.33,-47.26 -1892.52,-62.9 -1877,-81 C-1861.92,-98.59 -1848.2,-120.71 -1839.11,-146.38 C-1830.02,-172.04 -1825.57,-201.24 -1829,-233 C-1832.23,-262.84 -1840.89,-288.53 -1853.04,-310.41 C-1865.19,-332.3 -1880.82,-350.37 -1898,-365 C-1915.02,-379.5 -1937.52,-392.81 -1963.76,-401.84 C-1990.01,-410.88 -2020,-415.63 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2061.59,-412.21 -2070.55,-410.81 -2079.03,-408.89 C-2087.5,-406.97 -2095.48,-404.54 -2103.1,-401.69 C-2110.72,-398.84 -2117.98,-395.58 -2125,-392 C-2154.86,-376.79 -2181.38,-355.21 -2200.81,-326.98 C-2220.23,-298.75 -2232.55,-263.85 -2234,-222 C-2235.06,-191.38 -2230.21,-165.23 -2221.52,-142.55 C-2212.83,-119.88 -2200.3,-100.69 -2186,-84 C-2171.48,-67.06 -2154.35,-52.83 -2134.66,-41.45 C-2114.98,-30.08 -2092.74,-21.55 -2068,-16 C-2009.13,-2.8 -1958.21,-14.57 -1918.33,-39.75 C-1878.46,-64.93 -1849.64,-103.53 -1835,-144 C-1825.51,-170.22 -1822.73,-198.65 -1824.83,-225.69 C-1826.94,-252.72 -1833.93,-278.36 -1844,-299 C-1853.43,-318.34 -1865.87,-336.49 -1881.35,-352.36 C-1896.84,-368.23 -1915.37,-381.81 -1937,-392 C-1951.85,-398.99 -1969.2,-405.18 -1988.54,-409.15 C-2007.87,-413.12 -2029.2,-414.87 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="67"
+                    android:valueFrom="M-2052 -413 C-2061.59,-412.21 -2070.55,-410.81 -2079.03,-408.89 C-2087.5,-406.97 -2095.48,-404.54 -2103.1,-401.69 C-2110.72,-398.84 -2117.98,-395.58 -2125,-392 C-2154.86,-376.79 -2181.38,-355.21 -2200.81,-326.98 C-2220.23,-298.75 -2232.55,-263.85 -2234,-222 C-2235.06,-191.38 -2230.21,-165.23 -2221.52,-142.55 C-2212.83,-119.88 -2200.3,-100.69 -2186,-84 C-2171.48,-67.06 -2154.35,-52.83 -2134.66,-41.45 C-2114.98,-30.08 -2092.74,-21.55 -2068,-16 C-2009.13,-2.8 -1958.21,-14.57 -1918.33,-39.75 C-1878.46,-64.93 -1849.64,-103.53 -1835,-144 C-1825.51,-170.22 -1822.73,-198.65 -1824.83,-225.69 C-1826.94,-252.72 -1833.93,-278.36 -1844,-299 C-1853.43,-318.34 -1865.87,-336.49 -1881.35,-352.36 C-1896.84,-368.23 -1915.37,-381.81 -1937,-392 C-1951.85,-398.99 -1969.2,-405.18 -1988.54,-409.15 C-2007.87,-413.12 -2029.2,-414.87 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2063.16,-412.08 -2073.74,-410.37 -2083.77,-407.96 C-2093.79,-405.56 -2103.26,-402.47 -2112.17,-398.81 C-2121.07,-395.16 -2129.43,-390.93 -2137.23,-386.26 C-2145.03,-381.58 -2152.29,-376.46 -2159,-371 C-2169.4,-362.54 -2178.31,-353.88 -2186.05,-344.85 C-2193.79,-335.82 -2200.37,-326.42 -2206.08,-316.5 C-2211.8,-306.57 -2216.67,-296.13 -2221,-285 C-2238.37,-240.34 -2237.17,-196.9 -2225.73,-159.1 C-2214.28,-121.3 -2192.59,-89.13 -2169,-67 C-2142.29,-41.95 -2105.22,-23.59 -2065.35,-15.17 C-2025.49,-6.75 -1982.85,-8.27 -1945,-23 C-1912.4,-35.68 -1882.18,-56.87 -1859.35,-85.41 C-1836.52,-113.96 -1821.07,-149.86 -1818,-192 C-1815.28,-229.38 -1822.17,-262.27 -1834.17,-289.87 C-1846.17,-317.47 -1863.28,-339.78 -1881,-356 C-1899.69,-373.11 -1924.59,-388.83 -1953.74,-399.53 C-1982.89,-410.24 -2016.29,-415.93 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="83"
+                    android:valueFrom="M-2052 -413 C-2063.16,-412.08 -2073.74,-410.37 -2083.77,-407.96 C-2093.79,-405.56 -2103.26,-402.47 -2112.17,-398.81 C-2121.07,-395.16 -2129.43,-390.93 -2137.23,-386.26 C-2145.03,-381.58 -2152.29,-376.46 -2159,-371 C-2169.4,-362.54 -2178.31,-353.88 -2186.05,-344.85 C-2193.79,-335.82 -2200.37,-326.42 -2206.08,-316.5 C-2211.8,-306.57 -2216.67,-296.13 -2221,-285 C-2238.37,-240.34 -2237.17,-196.9 -2225.73,-159.1 C-2214.28,-121.3 -2192.59,-89.13 -2169,-67 C-2142.29,-41.95 -2105.22,-23.59 -2065.35,-15.17 C-2025.49,-6.75 -1982.85,-8.27 -1945,-23 C-1912.4,-35.68 -1882.18,-56.87 -1859.35,-85.41 C-1836.52,-113.96 -1821.07,-149.86 -1818,-192 C-1815.28,-229.38 -1822.17,-262.27 -1834.17,-289.87 C-1846.17,-317.47 -1863.28,-339.78 -1881,-356 C-1899.69,-373.11 -1924.59,-388.83 -1953.74,-399.53 C-1982.89,-410.24 -2016.29,-415.93 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2072.22,-411.34 -2091,-406.66 -2108.1,-399.87 C-2125.21,-393.08 -2140.66,-384.18 -2154.21,-374.07 C-2167.77,-363.95 -2179.44,-352.63 -2189,-341 C-2203.41,-323.47 -2216.63,-300.89 -2224.89,-274.11 C-2233.14,-247.32 -2236.44,-216.33 -2231,-182 C-2226.51,-153.63 -2217.1,-128.79 -2203.78,-107.33 C-2190.45,-85.88 -2173.19,-67.81 -2153,-53 C-2133.35,-38.58 -2108.08,-26.05 -2080.23,-17.76 C-2052.38,-9.48 -2021.96,-5.43 -1992,-8 C-1961.34,-10.63 -1935.39,-18.87 -1913.09,-31.22 C-1890.78,-43.56 -1872.12,-60 -1856,-79 C-1840.68,-97.06 -1827.09,-119.88 -1818.63,-146.65 C-1810.16,-173.42 -1806.82,-204.14 -1812,-238 C-1816.36,-266.51 -1826.56,-291.74 -1840.31,-313.35 C-1854.07,-334.96 -1871.39,-352.95 -1890,-367 C-1908.07,-380.64 -1932.92,-393.43 -1961.07,-402.16 C-1989.23,-410.89 -2020.69,-415.57 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="100"
+                    android:valueFrom="M-2052 -413 C-2072.22,-411.34 -2091,-406.66 -2108.1,-399.87 C-2125.21,-393.08 -2140.66,-384.18 -2154.21,-374.07 C-2167.77,-363.95 -2179.44,-352.63 -2189,-341 C-2203.41,-323.47 -2216.63,-300.89 -2224.89,-274.11 C-2233.14,-247.32 -2236.44,-216.33 -2231,-182 C-2226.51,-153.63 -2217.1,-128.79 -2203.78,-107.33 C-2190.45,-85.88 -2173.19,-67.81 -2153,-53 C-2133.35,-38.58 -2108.08,-26.05 -2080.23,-17.76 C-2052.38,-9.48 -2021.96,-5.43 -1992,-8 C-1961.34,-10.63 -1935.39,-18.87 -1913.09,-31.22 C-1890.78,-43.56 -1872.12,-60 -1856,-79 C-1840.68,-97.06 -1827.09,-119.88 -1818.63,-146.65 C-1810.16,-173.42 -1806.82,-204.14 -1812,-238 C-1816.36,-266.51 -1826.56,-291.74 -1840.31,-313.35 C-1854.07,-334.96 -1871.39,-352.95 -1890,-367 C-1908.07,-380.64 -1932.92,-393.43 -1961.07,-402.16 C-1989.23,-410.89 -2020.69,-415.57 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2083.49,-410.41 -2110.62,-400.99 -2133.57,-387.67 C-2156.51,-374.35 -2175.26,-357.14 -2190,-339 C-2204.11,-321.63 -2217.58,-298.34 -2225.92,-270.66 C-2234.26,-242.97 -2237.46,-210.91 -2231,-176 C-2223.12,-133.42 -2203.82,-99.93 -2176.63,-73.96 C-2149.45,-47.99 -2114.39,-29.53 -2075,-17 C-2060.09,-12.26 -2044.98,-8.59 -2029.65,-6.43 C-2014.33,-4.28 -1998.78,-3.65 -1983,-5 C-1951.72,-7.67 -1925.53,-16 -1903.13,-28.62 C-1880.73,-41.24 -1862.11,-58.16 -1846,-78 C-1830.98,-96.49 -1817.15,-119.48 -1808.74,-146.6 C-1800.33,-173.72 -1797.34,-204.98 -1804,-240 C-1809.39,-268.37 -1820.04,-293.46 -1834.27,-314.92 C-1848.5,-336.37 -1866.3,-354.19 -1886,-368 C-1896.13,-375.1 -1907.27,-380.86 -1919.42,-385.96 C-1931.58,-391.07 -1944.76,-395.51 -1959,-400 C-1987.83,-409.09 -2018.76,-415.73 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="117"
+                    android:valueFrom="M-2052 -413 C-2083.49,-410.41 -2110.62,-400.99 -2133.57,-387.67 C-2156.51,-374.35 -2175.26,-357.14 -2190,-339 C-2204.11,-321.63 -2217.58,-298.34 -2225.92,-270.66 C-2234.26,-242.97 -2237.46,-210.91 -2231,-176 C-2223.12,-133.42 -2203.82,-99.93 -2176.63,-73.96 C-2149.45,-47.99 -2114.39,-29.53 -2075,-17 C-2060.09,-12.26 -2044.98,-8.59 -2029.65,-6.43 C-2014.33,-4.28 -1998.78,-3.65 -1983,-5 C-1951.72,-7.67 -1925.53,-16 -1903.13,-28.62 C-1880.73,-41.24 -1862.11,-58.16 -1846,-78 C-1830.98,-96.49 -1817.15,-119.48 -1808.74,-146.6 C-1800.33,-173.72 -1797.34,-204.98 -1804,-240 C-1809.39,-268.37 -1820.04,-293.46 -1834.27,-314.92 C-1848.5,-336.37 -1866.3,-354.19 -1886,-368 C-1896.13,-375.1 -1907.27,-380.86 -1919.42,-385.96 C-1931.58,-391.07 -1944.76,-395.51 -1959,-400 C-1987.83,-409.09 -2018.76,-415.73 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2068.16,-411.67 -2083.44,-408.39 -2097.42,-403.83 C-2111.4,-399.27 -2124.07,-393.43 -2135,-387 C-2147.19,-379.82 -2157.4,-372.54 -2166.58,-364.44 C-2175.76,-356.34 -2183.92,-347.44 -2192,-337 C-2206.01,-318.91 -2219.24,-294.36 -2226.98,-265.73 C-2234.71,-237.1 -2236.95,-204.4 -2229,-170 C-2222.57,-142.18 -2211.58,-117.82 -2196.58,-96.96 C-2181.57,-76.11 -2162.54,-58.77 -2140,-45 C-2129.22,-38.41 -2117.43,-32.95 -2104.72,-27.98 C-2092.02,-23.02 -2078.4,-18.56 -2064,-14 C-2048.86,-9.2 -2033.94,-5.15 -2018.76,-2.7 C-2003.58,-0.26 -1988.15,0.6 -1972,-1 C-1909.86,-7.14 -1864.77,-35.48 -1833,-76 C-1803.02,-114.23 -1778.89,-172.33 -1794,-243 C-1806.16,-299.87 -1840.21,-341.67 -1882,-369 C-1902.73,-382.55 -1929.67,-390.89 -1958,-400 C-1986.85,-409.28 -2018.94,-415.71 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="133"
+                    android:valueFrom="M-2052 -413 C-2068.16,-411.67 -2083.44,-408.39 -2097.42,-403.83 C-2111.4,-399.27 -2124.07,-393.43 -2135,-387 C-2147.19,-379.82 -2157.4,-372.54 -2166.58,-364.44 C-2175.76,-356.34 -2183.92,-347.44 -2192,-337 C-2206.01,-318.91 -2219.24,-294.36 -2226.98,-265.73 C-2234.71,-237.1 -2236.95,-204.4 -2229,-170 C-2222.57,-142.18 -2211.58,-117.82 -2196.58,-96.96 C-2181.57,-76.11 -2162.54,-58.77 -2140,-45 C-2129.22,-38.41 -2117.43,-32.95 -2104.72,-27.98 C-2092.02,-23.02 -2078.4,-18.56 -2064,-14 C-2048.86,-9.2 -2033.94,-5.15 -2018.76,-2.7 C-2003.58,-0.26 -1988.15,0.6 -1972,-1 C-1909.86,-7.14 -1864.77,-35.48 -1833,-76 C-1803.02,-114.23 -1778.89,-172.33 -1794,-243 C-1806.16,-299.87 -1840.21,-341.67 -1882,-369 C-1902.73,-382.55 -1929.67,-390.89 -1958,-400 C-1986.85,-409.28 -2018.94,-415.71 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2068.7,-411.63 -2084.13,-408.26 -2098.16,-403.54 C-2112.2,-398.82 -2124.85,-392.76 -2136,-386 C-2147.98,-378.74 -2158.36,-371.57 -2167.81,-363.43 C-2177.26,-355.28 -2185.77,-346.16 -2194,-335 C-2207.5,-316.7 -2220.59,-290.87 -2228.01,-261.03 C-2235.43,-231.2 -2237.18,-197.35 -2228,-163 C-2220.91,-136.45 -2208.67,-112.16 -2192.46,-91.28 C-2176.24,-70.4 -2156.04,-52.92 -2133,-40 C-2121.53,-33.57 -2108.74,-28.52 -2095.21,-23.98 C-2081.68,-19.44 -2067.42,-15.41 -2053,-11 C-2036.87,-6.07 -2022.2,-1.69 -2007.14,1.02 C-1992.09,3.72 -1976.65,4.76 -1959,3 C-1895.61,-3.31 -1849.91,-32.53 -1818,-74 C-1787.82,-113.22 -1765.01,-174.6 -1783,-245 C-1797.41,-301.38 -1831.8,-344.78 -1876,-370 C-1898.81,-383.01 -1927.34,-389.73 -1956,-399 C-1985.38,-408.5 -2017.46,-415.84 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="150"
+                    android:valueFrom="M-2052 -413 C-2068.7,-411.63 -2084.13,-408.26 -2098.16,-403.54 C-2112.2,-398.82 -2124.85,-392.76 -2136,-386 C-2147.98,-378.74 -2158.36,-371.57 -2167.81,-363.43 C-2177.26,-355.28 -2185.77,-346.16 -2194,-335 C-2207.5,-316.7 -2220.59,-290.87 -2228.01,-261.03 C-2235.43,-231.2 -2237.18,-197.35 -2228,-163 C-2220.91,-136.45 -2208.67,-112.16 -2192.46,-91.28 C-2176.24,-70.4 -2156.04,-52.92 -2133,-40 C-2121.53,-33.57 -2108.74,-28.52 -2095.21,-23.98 C-2081.68,-19.44 -2067.42,-15.41 -2053,-11 C-2036.87,-6.07 -2022.2,-1.69 -2007.14,1.02 C-1992.09,3.72 -1976.65,4.76 -1959,3 C-1895.61,-3.31 -1849.91,-32.53 -1818,-74 C-1787.82,-113.22 -1765.01,-174.6 -1783,-245 C-1797.41,-301.38 -1831.8,-344.78 -1876,-370 C-1898.81,-383.01 -1927.34,-389.73 -1956,-399 C-1985.38,-408.5 -2017.46,-415.84 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2081.79,-410.55 -2107.78,-401.91 -2129.84,-389.82 C-2151.89,-377.73 -2169.99,-362.21 -2184,-346 C-2197.87,-329.95 -2211.62,-310.23 -2221.28,-286 C-2230.94,-261.78 -2236.5,-233.06 -2234,-199 C-2231.84,-169.67 -2223.83,-144.4 -2212.24,-122.59 C-2200.65,-100.79 -2185.48,-82.45 -2169,-67 C-2151.26,-50.36 -2130.31,-38.88 -2107.26,-29.54 C-2084.22,-20.21 -2059.09,-13.03 -2033,-5 C-2017.74,-0.3 -2004.84,3.33 -1991.59,5.6 C-1978.33,7.86 -1964.71,8.77 -1948,8 C-1905.51,6.05 -1868.9,-9.21 -1839.73,-31.86 C-1810.55,-54.51 -1788.79,-84.54 -1776,-116 C-1771.1,-128.04 -1767.01,-141.61 -1764.31,-155.76 C-1761.61,-169.9 -1760.31,-184.63 -1761,-199 C-1765.31,-289.18 -1818.08,-347.02 -1882,-375 C-1905.42,-385.25 -1933.09,-392.49 -1960,-401 C-1987.46,-409.69 -2016.78,-415.89 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="167"
+                    android:valueFrom="M-2052 -413 C-2081.79,-410.55 -2107.78,-401.91 -2129.84,-389.82 C-2151.89,-377.73 -2169.99,-362.21 -2184,-346 C-2197.87,-329.95 -2211.62,-310.23 -2221.28,-286 C-2230.94,-261.78 -2236.5,-233.06 -2234,-199 C-2231.84,-169.67 -2223.83,-144.4 -2212.24,-122.59 C-2200.65,-100.79 -2185.48,-82.45 -2169,-67 C-2151.26,-50.36 -2130.31,-38.88 -2107.26,-29.54 C-2084.22,-20.21 -2059.09,-13.03 -2033,-5 C-2017.74,-0.3 -2004.84,3.33 -1991.59,5.6 C-1978.33,7.86 -1964.71,8.77 -1948,8 C-1905.51,6.05 -1868.9,-9.21 -1839.73,-31.86 C-1810.55,-54.51 -1788.79,-84.54 -1776,-116 C-1771.1,-128.04 -1767.01,-141.61 -1764.31,-155.76 C-1761.61,-169.9 -1760.31,-184.63 -1761,-199 C-1765.31,-289.18 -1818.08,-347.02 -1882,-375 C-1905.42,-385.25 -1933.09,-392.49 -1960,-401 C-1987.46,-409.69 -2016.78,-415.89 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2069.88,-411.53 -2085.89,-407.88 -2100.3,-402.79 C-2114.71,-397.7 -2127.52,-391.19 -2139,-384 C-2162.8,-369.1 -2183.13,-350.89 -2198.84,-328.39 C-2214.56,-305.89 -2225.66,-279.09 -2231,-247 C-2234.03,-228.79 -2234.51,-211.38 -2232.87,-194.73 C-2231.22,-178.08 -2227.46,-162.19 -2222,-147 C-2212.49,-120.53 -2198.25,-96.52 -2179.64,-76.38 C-2161.02,-56.24 -2138.03,-39.97 -2111,-29 C-2097.39,-23.47 -2083.02,-18.98 -2068.36,-14.74 C-2053.7,-10.5 -2038.75,-6.51 -2024,-2 C-2009.05,2.58 -1994.19,7.3 -1978.25,10.39 C-1962.31,13.48 -1945.29,14.94 -1926,13 C-1858.97,6.24 -1812.71,-25.63 -1781,-71 C-1751.71,-112.9 -1731.01,-182.53 -1755,-252 C-1774.66,-308.93 -1813.12,-347.39 -1866,-370 C-1891.58,-380.93 -1922.29,-388.48 -1952,-398 C-1982.5,-407.77 -2015.77,-415.97 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="183"
+                    android:valueFrom="M-2052 -413 C-2069.88,-411.53 -2085.89,-407.88 -2100.3,-402.79 C-2114.71,-397.7 -2127.52,-391.19 -2139,-384 C-2162.8,-369.1 -2183.13,-350.89 -2198.84,-328.39 C-2214.56,-305.89 -2225.66,-279.09 -2231,-247 C-2234.03,-228.79 -2234.51,-211.38 -2232.87,-194.73 C-2231.22,-178.08 -2227.46,-162.19 -2222,-147 C-2212.49,-120.53 -2198.25,-96.52 -2179.64,-76.38 C-2161.02,-56.24 -2138.03,-39.97 -2111,-29 C-2097.39,-23.47 -2083.02,-18.98 -2068.36,-14.74 C-2053.7,-10.5 -2038.75,-6.51 -2024,-2 C-2009.05,2.58 -1994.19,7.3 -1978.25,10.39 C-1962.31,13.48 -1945.29,14.94 -1926,13 C-1858.97,6.24 -1812.71,-25.63 -1781,-71 C-1751.71,-112.9 -1731.01,-182.53 -1755,-252 C-1774.66,-308.93 -1813.12,-347.39 -1866,-370 C-1891.58,-380.93 -1922.29,-388.48 -1952,-398 C-1982.5,-407.77 -2015.77,-415.97 -2052,-413c "
+                    android:valueTo="M-2052 -413 C-2070.21,-411.5 -2086.7,-407.63 -2101.51,-402.32 C-2116.33,-397.01 -2129.47,-390.26 -2141,-383 C-2164.73,-368.06 -2185.58,-349.25 -2201.57,-325.58 C-2217.56,-301.91 -2228.69,-273.38 -2233,-239 C-2235.24,-221.1 -2234.79,-202.88 -2232.26,-185.49 C-2229.72,-168.1 -2225.1,-151.55 -2219,-137 C-2208.09,-110.99 -2192.21,-87.72 -2171.75,-68.46 C-2151.29,-49.2 -2126.25,-33.96 -2097,-24 C-2082.29,-18.99 -2067.22,-14.21 -2052.11,-9.58 C-2037.01,-4.94 -2021.86,-0.45 -2007,4 C-1992.53,8.33 -1976.87,12.99 -1960.04,16.12 C-1943.21,19.24 -1925.19,20.83 -1906,19 C-1837.8,12.51 -1789.31,-21.44 -1758,-68 C-1727.51,-113.34 -1711.25,-189.69 -1738,-256 C-1760.5,-311.77 -1803.61,-350.74 -1859,-370 C-1888.1,-380.12 -1918.72,-387.3 -1950,-397 C-1981.82,-406.86 -2013.69,-416.14 -2052,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="200"
+                    android:valueFrom="M-2052 -413 C-2070.21,-411.5 -2086.7,-407.63 -2101.51,-402.32 C-2116.33,-397.01 -2129.47,-390.26 -2141,-383 C-2164.73,-368.06 -2185.58,-349.25 -2201.57,-325.58 C-2217.56,-301.91 -2228.69,-273.38 -2233,-239 C-2235.24,-221.1 -2234.79,-202.88 -2232.26,-185.49 C-2229.72,-168.1 -2225.1,-151.55 -2219,-137 C-2208.09,-110.99 -2192.21,-87.72 -2171.75,-68.46 C-2151.29,-49.2 -2126.25,-33.96 -2097,-24 C-2082.29,-18.99 -2067.22,-14.21 -2052.11,-9.58 C-2037.01,-4.94 -2021.86,-0.45 -2007,4 C-1992.53,8.33 -1976.87,12.99 -1960.04,16.12 C-1943.21,19.24 -1925.19,20.83 -1906,19 C-1837.8,12.51 -1789.31,-21.44 -1758,-68 C-1727.51,-113.34 -1711.25,-189.69 -1738,-256 C-1760.5,-311.77 -1803.61,-350.74 -1859,-370 C-1888.1,-380.12 -1918.72,-387.3 -1950,-397 C-1981.82,-406.86 -2013.69,-416.14 -2052,-413c "
+                    android:valueTo="M-2048 -413 C-2066.96,-411.52 -2083.65,-407.87 -2098.63,-402.66 C-2113.62,-397.46 -2126.89,-390.7 -2139,-383 C-2163.5,-367.42 -2184.61,-348.88 -2200.66,-325.26 C-2216.71,-301.64 -2227.71,-272.93 -2232,-237 C-2234.19,-218.68 -2233.63,-200.16 -2230.93,-182.61 C-2228.24,-165.06 -2223.39,-148.47 -2217,-134 C-2205.27,-107.47 -2188.65,-84.32 -2167.4,-65.37 C-2146.15,-46.43 -2120.26,-31.69 -2090,-22 C-2075,-17.19 -2059.6,-12.47 -2044.27,-7.81 C-2028.95,-3.14 -2013.69,1.45 -1999,6 C-1983.7,10.73 -1967.89,16.01 -1951.43,20.09 C-1934.97,24.17 -1917.86,27.05 -1900,27 C-1823.07,26.78 -1770.47,-11.8 -1738,-57 C-1720.71,-81.07 -1708.33,-109.31 -1703,-145 C-1691.18,-224.09 -1728.76,-287.55 -1768,-323 C-1810.65,-361.52 -1879.25,-375.58 -1942,-395 C-1973.74,-404.82 -2008.85,-416.06 -2048,-413c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="217"
+                    android:valueFrom="M-2048 -413 C-2066.96,-411.52 -2083.65,-407.87 -2098.63,-402.66 C-2113.62,-397.46 -2126.89,-390.7 -2139,-383 C-2163.5,-367.42 -2184.61,-348.88 -2200.66,-325.26 C-2216.71,-301.64 -2227.71,-272.93 -2232,-237 C-2234.19,-218.68 -2233.63,-200.16 -2230.93,-182.61 C-2228.24,-165.06 -2223.39,-148.47 -2217,-134 C-2205.27,-107.47 -2188.65,-84.32 -2167.4,-65.37 C-2146.15,-46.43 -2120.26,-31.69 -2090,-22 C-2075,-17.19 -2059.6,-12.47 -2044.27,-7.81 C-2028.95,-3.14 -2013.69,1.45 -1999,6 C-1983.7,10.73 -1967.89,16.01 -1951.43,20.09 C-1934.97,24.17 -1917.86,27.05 -1900,27 C-1823.07,26.78 -1770.47,-11.8 -1738,-57 C-1720.71,-81.07 -1708.33,-109.31 -1703,-145 C-1691.18,-224.09 -1728.76,-287.55 -1768,-323 C-1810.65,-361.52 -1879.25,-375.58 -1942,-395 C-1973.74,-404.82 -2008.85,-416.06 -2048,-413c "
+                    android:valueTo="M-2044 -412 C-2063.62,-410.55 -2080.88,-406.72 -2096.49,-401.17 C-2112.09,-395.62 -2126.03,-388.34 -2139,-380 C-2164.03,-363.91 -2185.97,-343.3 -2202.06,-317.15 C-2218.15,-290.99 -2228.39,-259.29 -2230,-221 C-2231.65,-181.81 -2222.43,-147.58 -2207.28,-119.14 C-2192.13,-90.7 -2171.06,-68.05 -2149,-52 C-2136.68,-43.04 -2123.52,-36.2 -2109.14,-30.27 C-2094.77,-24.34 -2079.18,-19.32 -2062,-14 C-2046.23,-9.11 -2030.66,-4.02 -2014.94,1.05 C-1999.21,6.13 -1983.35,11.18 -1967,16 C-1950.6,20.83 -1933.88,26.02 -1916.12,29.66 C-1898.36,33.3 -1879.56,35.38 -1859,34 C-1785.73,29.09 -1734.33,-12.76 -1704,-61 C-1687.14,-87.82 -1677.54,-119.8 -1675,-157 C-1669.82,-232.93 -1712.56,-294.98 -1754,-326 C-1800.38,-360.72 -1874.96,-373.95 -1937,-393 C-1970.55,-403.3 -2003.97,-414.96 -2044,-412c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="233"
+                    android:valueFrom="M-2044 -412 C-2063.62,-410.55 -2080.88,-406.72 -2096.49,-401.17 C-2112.09,-395.62 -2126.03,-388.34 -2139,-380 C-2164.03,-363.91 -2185.97,-343.3 -2202.06,-317.15 C-2218.15,-290.99 -2228.39,-259.29 -2230,-221 C-2231.65,-181.81 -2222.43,-147.58 -2207.28,-119.14 C-2192.13,-90.7 -2171.06,-68.05 -2149,-52 C-2136.68,-43.04 -2123.52,-36.2 -2109.14,-30.27 C-2094.77,-24.34 -2079.18,-19.32 -2062,-14 C-2046.23,-9.11 -2030.66,-4.02 -2014.94,1.05 C-1999.21,6.13 -1983.35,11.18 -1967,16 C-1950.6,20.83 -1933.88,26.02 -1916.12,29.66 C-1898.36,33.3 -1879.56,35.38 -1859,34 C-1785.73,29.09 -1734.33,-12.76 -1704,-61 C-1687.14,-87.82 -1677.54,-119.8 -1675,-157 C-1669.82,-232.93 -1712.56,-294.98 -1754,-326 C-1800.38,-360.72 -1874.96,-373.95 -1937,-393 C-1970.55,-403.3 -2003.97,-414.96 -2044,-412c "
+                    android:valueTo="M-2043 -410 C-2062.47,-408.33 -2080.12,-404.05 -2096.08,-397.95 C-2112.04,-391.85 -2126.3,-383.94 -2139,-375 C-2163.84,-357.52 -2185.95,-334.8 -2201.41,-306.44 C-2216.88,-278.08 -2225.71,-244.07 -2224,-204 C-2222.32,-164.72 -2211.27,-131.77 -2194.29,-104.74 C-2177.32,-77.71 -2154.41,-56.59 -2129,-41 C-2115.9,-32.96 -2101.29,-26.6 -2085.64,-20.93 C-2069.99,-15.26 -2053.29,-10.28 -2036,-5 C-2019.94,-0.1 -2004.01,4.86 -1987.69,9.87 C-1971.37,14.87 -1954.65,19.92 -1937,25 C-1905.8,33.99 -1865.86,45.81 -1827,42 C-1752.62,34.71 -1698.64,-7.41 -1670,-59 C-1654,-87.82 -1644.06,-123.59 -1645,-163 C-1645.93,-201.81 -1658.75,-235.41 -1675,-262 C-1714,-325.81 -1760.88,-340.83 -1833,-362 C-1865.62,-371.58 -1898.36,-381.3 -1932,-392 C-1966.74,-403.05 -2001.4,-413.56 -2043,-410c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="250"
+                    android:valueFrom="M-2043 -410 C-2062.47,-408.33 -2080.12,-404.05 -2096.08,-397.95 C-2112.04,-391.85 -2126.3,-383.94 -2139,-375 C-2163.84,-357.52 -2185.95,-334.8 -2201.41,-306.44 C-2216.88,-278.08 -2225.71,-244.07 -2224,-204 C-2222.32,-164.72 -2211.27,-131.77 -2194.29,-104.74 C-2177.32,-77.71 -2154.41,-56.59 -2129,-41 C-2115.9,-32.96 -2101.29,-26.6 -2085.64,-20.93 C-2069.99,-15.26 -2053.29,-10.28 -2036,-5 C-2019.94,-0.1 -2004.01,4.86 -1987.69,9.87 C-1971.37,14.87 -1954.65,19.92 -1937,25 C-1905.8,33.99 -1865.86,45.81 -1827,42 C-1752.62,34.71 -1698.64,-7.41 -1670,-59 C-1654,-87.82 -1644.06,-123.59 -1645,-163 C-1645.93,-201.81 -1658.75,-235.41 -1675,-262 C-1714,-325.81 -1760.88,-340.83 -1833,-362 C-1865.62,-371.58 -1898.36,-381.3 -1932,-392 C-1966.74,-403.05 -2001.4,-413.56 -2043,-410c "
+                    android:valueTo="M-2028 -408 C-2048.69,-406.56 -2067.62,-402.42 -2084.53,-396.46 C-2101.45,-390.49 -2116.35,-382.71 -2129,-374 C-2153.83,-356.9 -2177.14,-333.29 -2193.47,-303.62 C-2209.79,-273.96 -2219.13,-238.26 -2216,-197 C-2212.99,-157.39 -2201.12,-124.14 -2183.05,-97.04 C-2164.99,-69.95 -2140.74,-49 -2113,-34 C-2099.07,-26.47 -2082.86,-20.28 -2065.99,-14.7 C-2049.12,-9.11 -2031.58,-4.13 -2015,1 C-1998.21,6.19 -1981.3,11.15 -1964.16,16.08 C-1947.03,21.01 -1929.67,25.91 -1912,31 C-1876.63,41.19 -1842.13,53.72 -1799,52 C-1720.79,48.87 -1665.28,1.83 -1636,-51 C-1619.77,-80.28 -1609.29,-118.72 -1612,-158 C-1617.51,-237.84 -1661.13,-292.87 -1714,-322 C-1743.39,-338.2 -1774.63,-344.74 -1813,-356 C-1847.11,-366.01 -1880.41,-376.12 -1915,-387 C-1949.7,-397.91 -1987.46,-410.81 -2028,-408c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="267"
+                    android:valueFrom="M-2028 -408 C-2048.69,-406.56 -2067.62,-402.42 -2084.53,-396.46 C-2101.45,-390.49 -2116.35,-382.71 -2129,-374 C-2153.83,-356.9 -2177.14,-333.29 -2193.47,-303.62 C-2209.79,-273.96 -2219.13,-238.26 -2216,-197 C-2212.99,-157.39 -2201.12,-124.14 -2183.05,-97.04 C-2164.99,-69.95 -2140.74,-49 -2113,-34 C-2099.07,-26.47 -2082.86,-20.28 -2065.99,-14.7 C-2049.12,-9.11 -2031.58,-4.13 -2015,1 C-1998.21,6.19 -1981.3,11.15 -1964.16,16.08 C-1947.03,21.01 -1929.67,25.91 -1912,31 C-1876.63,41.19 -1842.13,53.72 -1799,52 C-1720.79,48.87 -1665.28,1.83 -1636,-51 C-1619.77,-80.28 -1609.29,-118.72 -1612,-158 C-1617.51,-237.84 -1661.13,-292.87 -1714,-322 C-1743.39,-338.2 -1774.63,-344.74 -1813,-356 C-1847.11,-366.01 -1880.41,-376.12 -1915,-387 C-1949.7,-397.91 -1987.46,-410.81 -2028,-408c "
+                    android:valueTo="M-2009 -405 C-2051.87,-404.67 -2087.83,-392.14 -2116.71,-372.95 C-2145.6,-353.77 -2167.42,-327.93 -2182,-301 C-2190.39,-285.5 -2196.97,-267.54 -2201.03,-248.43 C-2205.1,-229.32 -2206.65,-209.07 -2205,-189 C-2201.67,-148.43 -2188.4,-114.91 -2168.6,-87.92 C-2148.8,-60.93 -2122.46,-40.46 -2093,-26 C-2077.81,-18.55 -2060.68,-12.37 -2043.04,-6.78 C-2025.39,-1.18 -2007.24,3.84 -1990,9 C-1972.83,14.14 -1955.59,19.23 -1938.01,24.37 C-1920.43,29.51 -1902.51,34.69 -1884,40 C-1848.91,50.07 -1808.39,64.36 -1769,63 C-1685.56,60.11 -1628.04,14.47 -1598,-41 C-1581.7,-71.11 -1570.85,-111.53 -1574,-152 C-1580.19,-231.59 -1628.54,-288.29 -1685,-316 C-1717.08,-331.74 -1751.19,-337.82 -1790,-349 C-1827.01,-359.66 -1860.79,-370.76 -1895,-381 C-1930.29,-391.56 -1967.46,-405.32 -2009,-405c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="283"
+                    android:valueFrom="M-2009 -405 C-2051.87,-404.67 -2087.83,-392.14 -2116.71,-372.95 C-2145.6,-353.77 -2167.42,-327.93 -2182,-301 C-2190.39,-285.5 -2196.97,-267.54 -2201.03,-248.43 C-2205.1,-229.32 -2206.65,-209.07 -2205,-189 C-2201.67,-148.43 -2188.4,-114.91 -2168.6,-87.92 C-2148.8,-60.93 -2122.46,-40.46 -2093,-26 C-2077.81,-18.55 -2060.68,-12.37 -2043.04,-6.78 C-2025.39,-1.18 -2007.24,3.84 -1990,9 C-1972.83,14.14 -1955.59,19.23 -1938.01,24.37 C-1920.43,29.51 -1902.51,34.69 -1884,40 C-1848.91,50.07 -1808.39,64.36 -1769,63 C-1685.56,60.11 -1628.04,14.47 -1598,-41 C-1581.7,-71.11 -1570.85,-111.53 -1574,-152 C-1580.19,-231.59 -1628.54,-288.29 -1685,-316 C-1717.08,-331.74 -1751.19,-337.82 -1790,-349 C-1827.01,-359.66 -1860.79,-370.76 -1895,-381 C-1930.29,-391.56 -1967.46,-405.32 -2009,-405c "
+                    android:valueTo="M-2011 -400 C-2053.42,-396.52 -2087.52,-381.94 -2114.34,-361.15 C-2141.17,-340.36 -2160.71,-313.35 -2174,-285 C-2181.72,-268.54 -2187.51,-250.33 -2190.47,-230.84 C-2193.44,-211.34 -2193.58,-190.57 -2190,-169 C-2183.7,-131.08 -2168.49,-98.81 -2147.08,-72.91 C-2125.67,-47.01 -2098.06,-27.47 -2067,-15 C-2050.55,-8.4 -2033.29,-3.15 -2015.51,1.83 C-1997.74,6.81 -1979.46,11.51 -1961,17 C-1926.67,27.21 -1890.41,37.86 -1854,48 C-1817.75,58.1 -1779.46,72.79 -1741,74 C-1651.6,76.8 -1593.42,34.23 -1560,-21 C-1542.31,-50.24 -1529.38,-88.39 -1530,-129 C-1531.26,-212.05 -1581.77,-272.22 -1635,-301 C-1665.71,-317.6 -1706.86,-326.2 -1739,-335 C-1775.31,-344.94 -1811.03,-355.54 -1846,-366 C-1882.13,-376.81 -1917.72,-389.72 -1954,-397 C-1969.82,-400.17 -1990.77,-401.66 -2011,-400c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="300"
+                    android:valueFrom="M-2011 -400 C-2053.42,-396.52 -2087.52,-381.94 -2114.34,-361.15 C-2141.17,-340.36 -2160.71,-313.35 -2174,-285 C-2181.72,-268.54 -2187.51,-250.33 -2190.47,-230.84 C-2193.44,-211.34 -2193.58,-190.57 -2190,-169 C-2183.7,-131.08 -2168.49,-98.81 -2147.08,-72.91 C-2125.67,-47.01 -2098.06,-27.47 -2067,-15 C-2050.55,-8.4 -2033.29,-3.15 -2015.51,1.83 C-1997.74,6.81 -1979.46,11.51 -1961,17 C-1926.67,27.21 -1890.41,37.86 -1854,48 C-1817.75,58.1 -1779.46,72.79 -1741,74 C-1651.6,76.8 -1593.42,34.23 -1560,-21 C-1542.31,-50.24 -1529.38,-88.39 -1530,-129 C-1531.26,-212.05 -1581.77,-272.22 -1635,-301 C-1665.71,-317.6 -1706.86,-326.2 -1739,-335 C-1775.31,-344.94 -1811.03,-355.54 -1846,-366 C-1882.13,-376.81 -1917.72,-389.72 -1954,-397 C-1969.82,-400.17 -1990.77,-401.66 -2011,-400c "
+                    android:valueTo="M-1993 -395 C-2035.52,-391.51 -2071.23,-376.75 -2099.59,-354.5 C-2127.94,-332.24 -2148.93,-302.49 -2162,-269 C-2168.66,-251.95 -2173.35,-230.87 -2174.9,-208.84 C-2176.46,-186.81 -2174.88,-163.83 -2169,-143 C-2158.02,-104.12 -2139.35,-74.22 -2113.89,-51.11 C-2088.42,-28.01 -2056.16,-11.7 -2018,0 C-1979.52,11.8 -1942.29,22.92 -1904.79,33.78 C-1867.3,44.64 -1829.54,55.24 -1790,66 C-1770.25,71.37 -1750.38,77.31 -1729.76,81.43 C-1709.14,85.56 -1687.76,87.87 -1665,86 C-1622.2,82.49 -1586.43,67.71 -1558.12,45.7 C-1529.81,23.68 -1508.96,-5.56 -1496,-38 C-1488.42,-56.98 -1484.03,-78.42 -1482.85,-100.14 C-1481.68,-121.87 -1483.72,-143.88 -1489,-164 C-1508.99,-240.16 -1563.51,-287.94 -1639,-308 C-1716.8,-328.67 -1792.02,-350.18 -1868,-373 C-1906.44,-384.54 -1946.52,-398.82 -1993,-395c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="317"
+                    android:valueFrom="M-1993 -395 C-2035.52,-391.51 -2071.23,-376.75 -2099.59,-354.5 C-2127.94,-332.24 -2148.93,-302.49 -2162,-269 C-2168.66,-251.95 -2173.35,-230.87 -2174.9,-208.84 C-2176.46,-186.81 -2174.88,-163.83 -2169,-143 C-2158.02,-104.12 -2139.35,-74.22 -2113.89,-51.11 C-2088.42,-28.01 -2056.16,-11.7 -2018,0 C-1979.52,11.8 -1942.29,22.92 -1904.79,33.78 C-1867.3,44.64 -1829.54,55.24 -1790,66 C-1770.25,71.37 -1750.38,77.31 -1729.76,81.43 C-1709.14,85.56 -1687.76,87.87 -1665,86 C-1622.2,82.49 -1586.43,67.71 -1558.12,45.7 C-1529.81,23.68 -1508.96,-5.56 -1496,-38 C-1488.42,-56.98 -1484.03,-78.42 -1482.85,-100.14 C-1481.68,-121.87 -1483.72,-143.88 -1489,-164 C-1508.99,-240.16 -1563.51,-287.94 -1639,-308 C-1716.8,-328.67 -1792.02,-350.18 -1868,-373 C-1906.44,-384.54 -1946.52,-398.82 -1993,-395c "
+                    android:valueTo="M-1972 -389 C-2015.53,-385.6 -2052.71,-370.1 -2082.05,-346.58 C-2111.38,-323.06 -2132.86,-291.51 -2145,-256 C-2148.29,-246.37 -2151.04,-236.62 -2152.97,-226.03 C-2154.89,-215.43 -2156,-204 -2156,-191 C-2156,-154.4 -2147.68,-123.61 -2134.39,-97.87 C-2121.1,-72.13 -2102.85,-51.43 -2083,-35 C-2068.94,-23.36 -2053.07,-14.7 -2035.52,-7.4 C-2017.98,-0.11 -1998.76,5.83 -1978,12 C-1938.06,23.88 -1899,35.2 -1859.7,46.27 C-1820.4,57.33 -1780.87,68.14 -1740,79 C-1699.56,89.74 -1657.14,104.77 -1609,101 C-1522.04,94.2 -1462.2,36.39 -1438,-32 C-1423.14,-74 -1423.94,-121.55 -1437,-161 C-1449.47,-198.68 -1467.69,-227.4 -1498,-253 C-1526.28,-276.89 -1561.4,-288.27 -1604,-299 C-1684.97,-319.39 -1764.33,-341.91 -1843,-365 C-1883.1,-376.77 -1923.46,-392.79 -1972,-389c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M-1972 -389 C-2015.53,-385.6 -2052.71,-370.1 -2082.05,-346.58 C-2111.38,-323.06 -2132.86,-291.51 -2145,-256 C-2148.29,-246.37 -2151.04,-236.62 -2152.97,-226.03 C-2154.89,-215.43 -2156,-204 -2156,-191 C-2156,-154.4 -2147.68,-123.61 -2134.39,-97.87 C-2121.1,-72.13 -2102.85,-51.43 -2083,-35 C-2068.94,-23.36 -2053.07,-14.7 -2035.52,-7.4 C-2017.98,-0.11 -1998.76,5.83 -1978,12 C-1938.06,23.88 -1899,35.2 -1859.7,46.27 C-1820.4,57.33 -1780.87,68.14 -1740,79 C-1699.56,89.74 -1657.14,104.77 -1609,101 C-1522.04,94.2 -1462.2,36.39 -1438,-32 C-1423.14,-74 -1423.94,-121.55 -1437,-161 C-1449.47,-198.68 -1467.69,-227.4 -1498,-253 C-1526.28,-276.89 -1561.4,-288.27 -1604,-299 C-1684.97,-319.39 -1764.33,-341.91 -1843,-365 C-1883.1,-376.77 -1923.46,-392.79 -1972,-389c "
+                    android:valueTo="M-1947 -382 C-1992.63,-378.43 -2031.23,-362.21 -2061.25,-337.24 C-2091.27,-312.27 -2112.7,-278.55 -2124,-240 C-2138.21,-191.5 -2133.14,-145.62 -2117.43,-107.06 C-2101.71,-68.5 -2075.36,-37.25 -2047,-18 C-2031.95,-7.78 -2013.76,0.26 -1994.19,7.23 C-1974.61,14.19 -1953.63,20.08 -1933,26 C-1891.74,37.84 -1850.85,49.22 -1809.47,60.33 C-1768.09,71.44 -1726.22,82.27 -1683,93 C-1662.04,98.2 -1640.37,104.9 -1618.02,109.82 C-1595.68,114.74 -1572.65,117.89 -1549,116 C-1503.84,112.4 -1465.18,96.01 -1435.01,71.21 C-1404.85,46.41 -1383.17,13.21 -1372,-24 C-1358.2,-69.97 -1362.29,-119.5 -1378,-158 C-1393.11,-195.04 -1414.99,-223.76 -1448,-247 C-1479.65,-269.28 -1518.77,-277.61 -1564,-289 C-1647.07,-309.92 -1731.94,-331.77 -1814,-356 C-1855.02,-368.11 -1897.6,-385.86 -1947,-382c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="350"
+                    android:valueFrom="M-1947 -382 C-1992.63,-378.43 -2031.23,-362.21 -2061.25,-337.24 C-2091.27,-312.27 -2112.7,-278.55 -2124,-240 C-2138.21,-191.5 -2133.14,-145.62 -2117.43,-107.06 C-2101.71,-68.5 -2075.36,-37.25 -2047,-18 C-2031.95,-7.78 -2013.76,0.26 -1994.19,7.23 C-1974.61,14.19 -1953.63,20.08 -1933,26 C-1891.74,37.84 -1850.85,49.22 -1809.47,60.33 C-1768.09,71.44 -1726.22,82.27 -1683,93 C-1662.04,98.2 -1640.37,104.9 -1618.02,109.82 C-1595.68,114.74 -1572.65,117.89 -1549,116 C-1503.84,112.4 -1465.18,96.01 -1435.01,71.21 C-1404.85,46.41 -1383.17,13.21 -1372,-24 C-1358.2,-69.97 -1362.29,-119.5 -1378,-158 C-1393.11,-195.04 -1414.99,-223.76 -1448,-247 C-1479.65,-269.28 -1518.77,-277.61 -1564,-289 C-1647.07,-309.92 -1731.94,-331.77 -1814,-356 C-1855.02,-368.11 -1897.6,-385.86 -1947,-382c "
+                    android:valueTo="M-1915 -374 C-1963.11,-370.66 -2003.62,-353.89 -2034.73,-327.71 C-2065.84,-301.53 -2087.53,-265.95 -2098,-225 C-2111.65,-171.6 -2104.14,-125.11 -2085.35,-87.34 C-2066.56,-49.57 -2036.48,-20.52 -2005,-2 C-1987.6,8.23 -1967.69,15.77 -1946.7,22.23 C-1925.72,28.68 -1903.67,34.07 -1882,40 C-1828.45,54.66 -1774.32,69.41 -1719.51,83.62 C-1664.71,97.83 -1609.24,111.5 -1553,124 C-1540.64,126.75 -1528.83,129.45 -1517.02,131.24 C-1505.2,133.02 -1493.38,133.9 -1481,133 C-1431.84,129.42 -1391.55,112.94 -1360.82,87.22 C-1330.09,61.5 -1308.92,26.54 -1298,-14 C-1284.79,-63.02 -1292.08,-115.37 -1310,-153 C-1326.77,-188.23 -1354.33,-219.1 -1389,-239 C-1423.4,-258.74 -1470.69,-267.55 -1515,-278 C-1603.39,-298.85 -1690.13,-322.03 -1777,-346 C-1817.41,-357.15 -1865.58,-377.43 -1915,-374c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="367"
+                    android:valueFrom="M-1915 -374 C-1963.11,-370.66 -2003.62,-353.89 -2034.73,-327.71 C-2065.84,-301.53 -2087.53,-265.95 -2098,-225 C-2111.65,-171.6 -2104.14,-125.11 -2085.35,-87.34 C-2066.56,-49.57 -2036.48,-20.52 -2005,-2 C-1987.6,8.23 -1967.69,15.77 -1946.7,22.23 C-1925.72,28.68 -1903.67,34.07 -1882,40 C-1828.45,54.66 -1774.32,69.41 -1719.51,83.62 C-1664.71,97.83 -1609.24,111.5 -1553,124 C-1540.64,126.75 -1528.83,129.45 -1517.02,131.24 C-1505.2,133.02 -1493.38,133.9 -1481,133 C-1431.84,129.42 -1391.55,112.94 -1360.82,87.22 C-1330.09,61.5 -1308.92,26.54 -1298,-14 C-1284.79,-63.02 -1292.08,-115.37 -1310,-153 C-1326.77,-188.23 -1354.33,-219.1 -1389,-239 C-1423.4,-258.74 -1470.69,-267.55 -1515,-278 C-1603.39,-298.85 -1690.13,-322.03 -1777,-346 C-1817.41,-357.15 -1865.58,-377.43 -1915,-374c "
+                    android:valueTo="M-1890 -364 C-1915.49,-361.91 -1938.32,-355.57 -1958.37,-346.18 C-1978.42,-336.78 -1995.67,-324.33 -2010,-310 C-2024.52,-295.48 -2037.55,-279.77 -2047.93,-261.42 C-2058.31,-243.07 -2066.05,-222.08 -2070,-197 C-2074.59,-167.83 -2073.02,-142.02 -2067.58,-119.16 C-2062.15,-96.3 -2052.85,-76.38 -2042,-59 C-2030.6,-40.74 -2017.28,-25.07 -2001.72,-11.92 C-1986.16,1.22 -1968.36,11.84 -1948,20 C-1905.62,36.98 -1860,46.68 -1815,59 C-1725.92,83.38 -1631.46,106.29 -1538,128 C-1491.98,138.69 -1441.13,155.58 -1390,151 C-1292.44,142.26 -1225.72,75.29 -1210,-14 C-1200.46,-68.19 -1215.31,-117.16 -1237,-153 C-1258.29,-188.17 -1290.09,-216.37 -1330,-233 C-1350.41,-241.5 -1374.56,-245.91 -1398,-251 C-1516.15,-276.67 -1631.32,-305.3 -1744,-337 C-1787.58,-349.26 -1835.91,-368.44 -1890,-364c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="383"
+                    android:valueFrom="M-1890 -364 C-1915.49,-361.91 -1938.32,-355.57 -1958.37,-346.18 C-1978.42,-336.78 -1995.67,-324.33 -2010,-310 C-2024.52,-295.48 -2037.55,-279.77 -2047.93,-261.42 C-2058.31,-243.07 -2066.05,-222.08 -2070,-197 C-2074.59,-167.83 -2073.02,-142.02 -2067.58,-119.16 C-2062.15,-96.3 -2052.85,-76.38 -2042,-59 C-2030.6,-40.74 -2017.28,-25.07 -2001.72,-11.92 C-1986.16,1.22 -1968.36,11.84 -1948,20 C-1905.62,36.98 -1860,46.68 -1815,59 C-1725.92,83.38 -1631.46,106.29 -1538,128 C-1491.98,138.69 -1441.13,155.58 -1390,151 C-1292.44,142.26 -1225.72,75.29 -1210,-14 C-1200.46,-68.19 -1215.31,-117.16 -1237,-153 C-1258.29,-188.17 -1290.09,-216.37 -1330,-233 C-1350.41,-241.5 -1374.56,-245.91 -1398,-251 C-1516.15,-276.67 -1631.32,-305.3 -1744,-337 C-1787.58,-349.26 -1835.91,-368.44 -1890,-364c "
+                    android:valueTo="M-1841 -354 C-1869.44,-353.16 -1894.26,-347.31 -1915.8,-337.97 C-1937.35,-328.63 -1955.63,-315.8 -1971,-301 C-1986.17,-286.39 -1999.54,-271.29 -2010.18,-252.51 C-2020.82,-233.74 -2028.74,-211.3 -2033,-182 C-2041.16,-125.91 -2026.91,-77.42 -2000.49,-39.83 C-1974.07,-2.23 -1935.49,24.47 -1895,37 C-1871.46,44.28 -1847.67,50.61 -1823.78,56.71 C-1799.89,62.8 -1775.91,68.66 -1752,75 C-1632.18,106.77 -1509.29,135.8 -1384,161 C-1357.24,166.38 -1331.35,172.76 -1304,172 C-1248.4,170.45 -1205.4,147.31 -1175,120 C-1143.38,91.59 -1119.42,51.91 -1112,2 C-1103.47,-55.42 -1120.14,-106.53 -1145,-142 C-1169.17,-176.48 -1205.94,-205.29 -1250,-219 C-1273.89,-226.43 -1299.44,-229.88 -1324,-235 C-1449.37,-261.13 -1570.6,-289.56 -1691,-322 C-1735.34,-333.94 -1789.57,-355.52 -1841,-354c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="400"
+                    android:valueFrom="M-1841 -354 C-1869.44,-353.16 -1894.26,-347.31 -1915.8,-337.97 C-1937.35,-328.63 -1955.63,-315.8 -1971,-301 C-1986.17,-286.39 -1999.54,-271.29 -2010.18,-252.51 C-2020.82,-233.74 -2028.74,-211.3 -2033,-182 C-2041.16,-125.91 -2026.91,-77.42 -2000.49,-39.83 C-1974.07,-2.23 -1935.49,24.47 -1895,37 C-1871.46,44.28 -1847.67,50.61 -1823.78,56.71 C-1799.89,62.8 -1775.91,68.66 -1752,75 C-1632.18,106.77 -1509.29,135.8 -1384,161 C-1357.24,166.38 -1331.35,172.76 -1304,172 C-1248.4,170.45 -1205.4,147.31 -1175,120 C-1143.38,91.59 -1119.42,51.91 -1112,2 C-1103.47,-55.42 -1120.14,-106.53 -1145,-142 C-1169.17,-176.48 -1205.94,-205.29 -1250,-219 C-1273.89,-226.43 -1299.44,-229.88 -1324,-235 C-1449.37,-261.13 -1570.6,-289.56 -1691,-322 C-1735.34,-333.94 -1789.57,-355.52 -1841,-354c "
+                    android:valueTo="M-1796 -342 C-1811.94,-341.79 -1825.94,-339.83 -1838.82,-336.66 C-1851.71,-333.48 -1863.49,-329.09 -1875,-324 C-1907.54,-309.62 -1935.19,-288.4 -1955.51,-260 C-1975.82,-231.61 -1988.8,-196.05 -1992,-153 C-1994.25,-122.72 -1989.36,-95.65 -1980.35,-72.07 C-1971.34,-48.48 -1958.22,-28.37 -1944,-12 C-1928.17,6.22 -1910.48,19.99 -1890.22,30.95 C-1869.96,41.91 -1847.13,50.05 -1821,57 C-1732.48,80.56 -1643.53,102.75 -1552.93,123.58 C-1462.34,144.4 -1370.1,163.87 -1275,182 C-1247.01,187.34 -1218.58,194.81 -1191,194 C-1131.03,192.25 -1087.63,168.15 -1056,138 C-1024.96,108.42 -997.59,63.62 -995,7 C-992.15,-55.28 -1013.07,-100.38 -1043,-136 C-1072.69,-171.34 -1113.24,-194.03 -1167,-204 C-1328.4,-233.93 -1484.44,-268.19 -1638,-308 C-1687.14,-320.74 -1741.73,-342.71 -1796,-342c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="417"
+                    android:valueFrom="M-1796 -342 C-1811.94,-341.79 -1825.94,-339.83 -1838.82,-336.66 C-1851.71,-333.48 -1863.49,-329.09 -1875,-324 C-1907.54,-309.62 -1935.19,-288.4 -1955.51,-260 C-1975.82,-231.61 -1988.8,-196.05 -1992,-153 C-1994.25,-122.72 -1989.36,-95.65 -1980.35,-72.07 C-1971.34,-48.48 -1958.22,-28.37 -1944,-12 C-1928.17,6.22 -1910.48,19.99 -1890.22,30.95 C-1869.96,41.91 -1847.13,50.05 -1821,57 C-1732.48,80.56 -1643.53,102.75 -1552.93,123.58 C-1462.34,144.4 -1370.1,163.87 -1275,182 C-1247.01,187.34 -1218.58,194.81 -1191,194 C-1131.03,192.25 -1087.63,168.15 -1056,138 C-1024.96,108.42 -997.59,63.62 -995,7 C-992.15,-55.28 -1013.07,-100.38 -1043,-136 C-1072.69,-171.34 -1113.24,-194.03 -1167,-204 C-1328.4,-233.93 -1484.44,-268.19 -1638,-308 C-1687.14,-320.74 -1741.73,-342.71 -1796,-342c "
+                    android:valueTo="M-1758 -328 C-1773.72,-326.77 -1788.05,-324.1 -1801.28,-320.2 C-1814.52,-316.3 -1826.65,-311.16 -1838,-305 C-1867.83,-288.8 -1896.87,-263.63 -1916.93,-230.09 C-1936.99,-196.55 -1948.07,-154.65 -1942,-105 C-1938.26,-74.41 -1929.05,-49.04 -1916.25,-27.63 C-1903.46,-6.22 -1887.08,11.24 -1869,26 C-1849.22,42.16 -1826.74,52.66 -1802.07,61 C-1777.41,69.34 -1750.55,75.51 -1722,83 C-1627.76,107.73 -1531.68,130.26 -1433.57,150.84 C-1335.47,171.42 -1235.33,190.06 -1133,207 C-1102.48,212.05 -1071.97,219.42 -1041,217 C-980.71,212.29 -935.73,182.99 -906,149 C-876.19,114.92 -850.64,62 -858,-5 C-867.57,-92.16 -925.05,-149.77 -996,-173 C-1020.84,-181.13 -1049.84,-184.7 -1081,-190 C-1283.95,-224.53 -1479.06,-266.92 -1668,-316 C-1696.45,-323.39 -1726.55,-330.46 -1758,-328c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="433"
+                    android:valueFrom="M-1758 -328 C-1773.72,-326.77 -1788.05,-324.1 -1801.28,-320.2 C-1814.52,-316.3 -1826.65,-311.16 -1838,-305 C-1867.83,-288.8 -1896.87,-263.63 -1916.93,-230.09 C-1936.99,-196.55 -1948.07,-154.65 -1942,-105 C-1938.26,-74.41 -1929.05,-49.04 -1916.25,-27.63 C-1903.46,-6.22 -1887.08,11.24 -1869,26 C-1849.22,42.16 -1826.74,52.66 -1802.07,61 C-1777.41,69.34 -1750.55,75.51 -1722,83 C-1627.76,107.73 -1531.68,130.26 -1433.57,150.84 C-1335.47,171.42 -1235.33,190.06 -1133,207 C-1102.48,212.05 -1071.97,219.42 -1041,217 C-980.71,212.29 -935.73,182.99 -906,149 C-876.19,114.92 -850.64,62 -858,-5 C-867.57,-92.16 -925.05,-149.77 -996,-173 C-1020.84,-181.13 -1049.84,-184.7 -1081,-190 C-1283.95,-224.53 -1479.06,-266.92 -1668,-316 C-1696.45,-323.39 -1726.55,-330.46 -1758,-328c "
+                    android:valueTo="M-1702 -313 C-1718.97,-311.67 -1734.27,-308.63 -1748.31,-304.2 C-1762.34,-299.76 -1775.11,-293.92 -1787,-287 C-1818.73,-268.52 -1848.63,-238.43 -1867.4,-200.25 C-1886.16,-162.07 -1893.8,-115.81 -1881,-65 C-1873.66,-35.86 -1861.23,-11.42 -1844.97,9 C-1828.71,29.43 -1808.63,45.86 -1786,59 C-1763.78,71.9 -1735.76,80.93 -1706.18,88.48 C-1676.6,96.02 -1645.45,102.07 -1617,109 C-1514.25,134.02 -1409.51,156.86 -1302.58,177.39 C-1195.65,197.93 -1086.52,216.17 -975,232 C-941.5,236.76 -907.26,243.23 -874,241 C-807.88,236.57 -764.02,203.88 -733,164 C-703.96,126.67 -678.96,62.64 -696,-6 C-710.57,-64.7 -744.81,-104.43 -790,-131 C-838.34,-159.43 -903.19,-162.4 -968,-172 C-1188.55,-204.67 -1401.48,-249.48 -1605,-300 C-1635.42,-307.55 -1668.51,-315.62 -1702,-313c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="450"
+                    android:valueFrom="M-1702 -313 C-1718.97,-311.67 -1734.27,-308.63 -1748.31,-304.2 C-1762.34,-299.76 -1775.11,-293.92 -1787,-287 C-1818.73,-268.52 -1848.63,-238.43 -1867.4,-200.25 C-1886.16,-162.07 -1893.8,-115.81 -1881,-65 C-1873.66,-35.86 -1861.23,-11.42 -1844.97,9 C-1828.71,29.43 -1808.63,45.86 -1786,59 C-1763.78,71.9 -1735.76,80.93 -1706.18,88.48 C-1676.6,96.02 -1645.45,102.07 -1617,109 C-1514.25,134.02 -1409.51,156.86 -1302.58,177.39 C-1195.65,197.93 -1086.52,216.17 -975,232 C-941.5,236.76 -907.26,243.23 -874,241 C-807.88,236.57 -764.02,203.88 -733,164 C-703.96,126.67 -678.96,62.64 -696,-6 C-710.57,-64.7 -744.81,-104.43 -790,-131 C-838.34,-159.43 -903.19,-162.4 -968,-172 C-1188.55,-204.67 -1401.48,-249.48 -1605,-300 C-1635.42,-307.55 -1668.51,-315.62 -1702,-313c "
+                    android:valueTo="M-1629 -297 C-1647.68,-296 -1664.53,-292.94 -1679.91,-288.29 C-1695.29,-283.64 -1709.2,-277.39 -1722,-270 C-1735.13,-262.42 -1746.45,-254.66 -1756.69,-245.68 C-1766.94,-236.7 -1776.13,-226.49 -1785,-214 C-1801.02,-191.45 -1813.31,-160.96 -1818.44,-127.98 C-1823.58,-95 -1821.57,-59.52 -1809,-27 C-1798.02,1.4 -1782.25,25.85 -1762.24,45.72 C-1742.22,65.59 -1717.96,80.88 -1690,91 C-1661.64,101.26 -1627.93,107.27 -1595,115 C-1338.91,175.14 -1069.51,224.52 -786,256 C-749.41,260.06 -712.09,266.16 -678,265 C-602.8,262.44 -555.29,227.14 -523,183 C-491.08,139.36 -471.68,63.07 -497,-4 C-518,-59.64 -556.44,-100.48 -616,-122 C-645.9,-132.8 -681.94,-135 -718,-139 C-999.71,-170.28 -1271.16,-219.92 -1527,-280 C-1559.85,-287.71 -1594.62,-298.85 -1629,-297c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="467"
+                    android:valueFrom="M-1629 -297 C-1647.68,-296 -1664.53,-292.94 -1679.91,-288.29 C-1695.29,-283.64 -1709.2,-277.39 -1722,-270 C-1735.13,-262.42 -1746.45,-254.66 -1756.69,-245.68 C-1766.94,-236.7 -1776.13,-226.49 -1785,-214 C-1801.02,-191.45 -1813.31,-160.96 -1818.44,-127.98 C-1823.58,-95 -1821.57,-59.52 -1809,-27 C-1798.02,1.4 -1782.25,25.85 -1762.24,45.72 C-1742.22,65.59 -1717.96,80.88 -1690,91 C-1661.64,101.26 -1627.93,107.27 -1595,115 C-1338.91,175.14 -1069.51,224.52 -786,256 C-749.41,260.06 -712.09,266.16 -678,265 C-602.8,262.44 -555.29,227.14 -523,183 C-491.08,139.36 -471.68,63.07 -497,-4 C-518,-59.64 -556.44,-100.48 -616,-122 C-645.9,-132.8 -681.94,-135 -718,-139 C-999.71,-170.28 -1271.16,-219.92 -1527,-280 C-1559.85,-287.71 -1594.62,-298.85 -1629,-297c "
+                    android:valueTo="M-1554 -279 C-1573.95,-278.03 -1592.16,-274.76 -1608.9,-269.49 C-1625.63,-264.23 -1640.91,-256.96 -1655,-248 C-1681.11,-231.4 -1704.11,-209.28 -1720.58,-181.38 C-1737.06,-153.48 -1747,-119.78 -1747,-80 C-1747,-40.36 -1736.63,-6.11 -1720.07,22.22 C-1703.51,50.56 -1680.76,72.99 -1656,89 C-1642.79,97.54 -1627.94,103.93 -1611.81,109.23 C-1595.69,114.54 -1578.3,118.77 -1560,123 C-1243.1,196.29 -905.52,251.65 -545,279 C-503.41,282.15 -461.51,286.97 -421,286 C-342.32,284.12 -289.14,239.96 -258,189 C-241.23,161.57 -229.41,126.9 -229,88 C-228.57,48.09 -239.97,14.95 -257,-14 C-288.07,-66.84 -336.69,-106.64 -418,-113 C-495.07,-119.03 -576.44,-125.73 -659,-133 C-929.35,-156.79 -1195.73,-207.11 -1442,-260 C-1478.55,-267.85 -1516.39,-280.83 -1554,-279c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="483"
+                    android:valueFrom="M-1554 -279 C-1573.95,-278.03 -1592.16,-274.76 -1608.9,-269.49 C-1625.63,-264.23 -1640.91,-256.96 -1655,-248 C-1681.11,-231.4 -1704.11,-209.28 -1720.58,-181.38 C-1737.06,-153.48 -1747,-119.78 -1747,-80 C-1747,-40.36 -1736.63,-6.11 -1720.07,22.22 C-1703.51,50.56 -1680.76,72.99 -1656,89 C-1642.79,97.54 -1627.94,103.93 -1611.81,109.23 C-1595.69,114.54 -1578.3,118.77 -1560,123 C-1243.1,196.29 -905.52,251.65 -545,279 C-503.41,282.15 -461.51,286.97 -421,286 C-342.32,284.12 -289.14,239.96 -258,189 C-241.23,161.57 -229.41,126.9 -229,88 C-228.57,48.09 -239.97,14.95 -257,-14 C-288.07,-66.84 -336.69,-106.64 -418,-113 C-495.07,-119.03 -576.44,-125.73 -659,-133 C-929.35,-156.79 -1195.73,-207.11 -1442,-260 C-1478.55,-267.85 -1516.39,-280.83 -1554,-279c "
+                    android:valueTo="M-1475 -259 C-1497.94,-257.3 -1517.43,-252.76 -1534.68,-246.04 C-1551.94,-239.32 -1566.97,-230.42 -1581,-220 C-1607.62,-200.23 -1631.26,-172.32 -1645.92,-138.18 C-1660.58,-104.04 -1666.27,-63.68 -1657,-19 C-1649.07,19.22 -1632.37,51.34 -1608.87,76.59 C-1585.37,101.84 -1555.09,120.22 -1520,131 C-1502.34,136.42 -1483.19,140.77 -1463.28,144.83 C-1443.37,148.9 -1422.7,152.69 -1402,157 C-1023.46,235.88 -622.54,284.09 -179,298 C-155.11,298.75 -130.67,300.57 -108,299 C-25.73,293.3 33.98,241.56 61,181 C76.53,146.2 82.53,100.81 74,59 C58.4,-17.45 8.72,-69.55 -62,-92 C-97.88,-103.39 -148.34,-103 -195,-103 C-282.94,-103 -387.12,-111.21 -468,-117 C-774.59,-138.96 -1075.42,-182.91 -1350,-240 C-1389.82,-248.28 -1432.72,-262.13 -1475,-259c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="500"
+                    android:valueFrom="M-1475 -259 C-1497.94,-257.3 -1517.43,-252.76 -1534.68,-246.04 C-1551.94,-239.32 -1566.97,-230.42 -1581,-220 C-1607.62,-200.23 -1631.26,-172.32 -1645.92,-138.18 C-1660.58,-104.04 -1666.27,-63.68 -1657,-19 C-1649.07,19.22 -1632.37,51.34 -1608.87,76.59 C-1585.37,101.84 -1555.09,120.22 -1520,131 C-1502.34,136.42 -1483.19,140.77 -1463.28,144.83 C-1443.37,148.9 -1422.7,152.69 -1402,157 C-1023.46,235.88 -622.54,284.09 -179,298 C-155.11,298.75 -130.67,300.57 -108,299 C-25.73,293.3 33.98,241.56 61,181 C76.53,146.2 82.53,100.81 74,59 C58.4,-17.45 8.72,-69.55 -62,-92 C-97.88,-103.39 -148.34,-103 -195,-103 C-282.94,-103 -387.12,-111.21 -468,-117 C-774.59,-138.96 -1075.42,-182.91 -1350,-240 C-1389.82,-248.28 -1432.72,-262.13 -1475,-259c "
+                    android:valueTo="M-1373 -238 C-1397.37,-236.31 -1418.62,-231.33 -1437.53,-223.76 C-1456.44,-216.19 -1473,-206.04 -1488,-194 C-1503.21,-181.79 -1516.33,-168.45 -1527.22,-153.33 C-1538.1,-138.21 -1546.75,-121.31 -1553,-102 C-1568.58,-53.91 -1564.76,-8.07 -1550.21,30.82 C-1535.66,69.72 -1510.38,101.67 -1483,122 C-1452.82,144.41 -1414.34,154.7 -1368,164 C-1237.54,190.18 -1096.12,213.69 -965,233 C-643.35,280.36 -299.33,300 68,300 C120,300 172,299.45 222,296 C316.17,289.51 375.99,234.85 401,161 C433.46,65.17 388.09,-21.48 333,-63 C304.2,-84.71 265.24,-102.48 216,-103 C161,-103.58 109.46,-100 62,-100 C-42.22,-100 -151.92,-100.95 -248,-105 C-597.18,-119.72 -929.37,-158.41 -1236,-217 C-1278.46,-225.11 -1327.89,-241.13 -1373,-238c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="517"
+                    android:valueFrom="M-1373 -238 C-1397.37,-236.31 -1418.62,-231.33 -1437.53,-223.76 C-1456.44,-216.19 -1473,-206.04 -1488,-194 C-1503.21,-181.79 -1516.33,-168.45 -1527.22,-153.33 C-1538.1,-138.21 -1546.75,-121.31 -1553,-102 C-1568.58,-53.91 -1564.76,-8.07 -1550.21,30.82 C-1535.66,69.72 -1510.38,101.67 -1483,122 C-1452.82,144.41 -1414.34,154.7 -1368,164 C-1237.54,190.18 -1096.12,213.69 -965,233 C-643.35,280.36 -299.33,300 68,300 C120,300 172,299.45 222,296 C316.17,289.51 375.99,234.85 401,161 C433.46,65.17 388.09,-21.48 333,-63 C304.2,-84.71 265.24,-102.48 216,-103 C161,-103.58 109.46,-100 62,-100 C-42.22,-100 -151.92,-100.95 -248,-105 C-597.18,-119.72 -929.37,-158.41 -1236,-217 C-1278.46,-225.11 -1327.89,-241.13 -1373,-238c "
+                    android:valueTo="M-1265 -215 C-1290.3,-212.92 -1313.06,-206.47 -1333,-197.26 C-1352.93,-188.06 -1370.03,-176.1 -1384,-163 C-1398.71,-149.21 -1411.64,-133.82 -1422.06,-116.08 C-1432.48,-98.34 -1440.37,-78.24 -1445,-55 C-1455.6,-1.79 -1444.84,45.36 -1423.06,83.13 C-1401.29,120.91 -1368.49,149.3 -1335,165 C-1316.23,173.8 -1293.86,179.41 -1270.12,183.89 C-1246.37,188.37 -1221.26,191.72 -1197,196 C-1052.86,221.45 -899.36,243.11 -753,259 C-353.75,302.34 120.02,314.63 538,279 C633.75,270.84 700.5,206.17 718,121 C740.05,13.71 679.18,-65.56 609,-100 C589.87,-109.39 569.5,-116.07 544,-119 C520.02,-121.75 489.94,-117.74 464,-116 C409.15,-112.32 351.08,-109.29 304,-107 C-196.98,-82.59 -686.9,-120.79 -1115,-195 C-1161.65,-203.09 -1214.46,-219.15 -1265,-215c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="533"
+                    android:valueFrom="M-1265 -215 C-1290.3,-212.92 -1313.06,-206.47 -1333,-197.26 C-1352.93,-188.06 -1370.03,-176.1 -1384,-163 C-1398.71,-149.21 -1411.64,-133.82 -1422.06,-116.08 C-1432.48,-98.34 -1440.37,-78.24 -1445,-55 C-1455.6,-1.79 -1444.84,45.36 -1423.06,83.13 C-1401.29,120.91 -1368.49,149.3 -1335,165 C-1316.23,173.8 -1293.86,179.41 -1270.12,183.89 C-1246.37,188.37 -1221.26,191.72 -1197,196 C-1052.86,221.45 -899.36,243.11 -753,259 C-353.75,302.34 120.02,314.63 538,279 C633.75,270.84 700.5,206.17 718,121 C740.05,13.71 679.18,-65.56 609,-100 C589.87,-109.39 569.5,-116.07 544,-119 C520.02,-121.75 489.94,-117.74 464,-116 C409.15,-112.32 351.08,-109.29 304,-107 C-196.98,-82.59 -686.9,-120.79 -1115,-195 C-1161.65,-203.09 -1214.46,-219.15 -1265,-215c "
+                    android:valueTo="M-1118 -192 C-1145.42,-191.79 -1170.54,-186.13 -1192.5,-177.22 C-1214.45,-168.32 -1233.24,-156.17 -1248,-143 C-1263.27,-129.37 -1277,-113.05 -1288.05,-94.18 C-1299.1,-75.3 -1307.46,-53.87 -1312,-30 C-1332.86,79.71 -1267.54,162.8 -1189,193 C-1147.25,209.05 -1092.45,214.06 -1041,222 C-887.29,245.72 -722.44,264.69 -565,277 C-240.57,302.37 128.06,309.73 459,285 C570.39,276.68 679.51,267.39 784,256 C887.81,244.69 957.02,188.83 976,94 C987.3,37.57 972.22,-12.23 950,-49 C917.49,-102.8 862.33,-144.29 781,-145 C750.06,-145.27 727.56,-139.91 700,-137 C541.6,-120.26 360.16,-106.76 203,-103 C92.15,-100.34 -28.22,-98.3 -146,-102 C-465.5,-112.02 -757.79,-137.12 -1038,-183 C-1064.09,-187.27 -1095.31,-192.17 -1118,-192c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="550"
+                    android:valueFrom="M-1118 -192 C-1145.42,-191.79 -1170.54,-186.13 -1192.5,-177.22 C-1214.45,-168.32 -1233.24,-156.17 -1248,-143 C-1263.27,-129.37 -1277,-113.05 -1288.05,-94.18 C-1299.1,-75.3 -1307.46,-53.87 -1312,-30 C-1332.86,79.71 -1267.54,162.8 -1189,193 C-1147.25,209.05 -1092.45,214.06 -1041,222 C-887.29,245.72 -722.44,264.69 -565,277 C-240.57,302.37 128.06,309.73 459,285 C570.39,276.68 679.51,267.39 784,256 C887.81,244.69 957.02,188.83 976,94 C987.3,37.57 972.22,-12.23 950,-49 C917.49,-102.8 862.33,-144.29 781,-145 C750.06,-145.27 727.56,-139.91 700,-137 C541.6,-120.26 360.16,-106.76 203,-103 C92.15,-100.34 -28.22,-98.3 -146,-102 C-465.5,-112.02 -757.79,-137.12 -1038,-183 C-1064.09,-187.27 -1095.31,-192.17 -1118,-192c "
+                    android:valueTo="M970 -171 C943.29,-168.81 916.34,-163.76 889,-160 C702.71,-134.36 510.97,-116.08 311,-107 C84.92,-96.73 -172.89,-100.57 -391,-112 C-497.92,-117.6 -602.84,-127.1 -723,-140 C-773.04,-145.37 -825.42,-152.52 -883,-160 C-909.72,-163.47 -935.68,-168.58 -965,-168 C-996.16,-167.38 -1018.31,-161.2 -1040,-152 C-1115.75,-119.87 -1181.79,-30.18 -1153,82 C-1140.72,129.86 -1115.71,165.17 -1082,191 C-1045.71,218.81 -1002.91,227.92 -946,236 C-630.61,280.78 -288.29,300 71,300 C188.01,300 304.89,292.44 419,287 C585.2,279.08 754.78,261.17 912,240 C964.06,232.99 1024.03,228.96 1065,212 C1142.61,179.87 1208.86,90.77 1181,-21 C1169.65,-66.53 1143.86,-103.88 1110,-130 C1077.04,-155.43 1028.35,-175.79 970,-171c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="567"
+                    android:valueFrom="M970 -171 C943.29,-168.81 916.34,-163.76 889,-160 C702.71,-134.36 510.97,-116.08 311,-107 C84.92,-96.73 -172.89,-100.57 -391,-112 C-497.92,-117.6 -602.84,-127.1 -723,-140 C-773.04,-145.37 -825.42,-152.52 -883,-160 C-909.72,-163.47 -935.68,-168.58 -965,-168 C-996.16,-167.38 -1018.31,-161.2 -1040,-152 C-1115.75,-119.87 -1181.79,-30.18 -1153,82 C-1140.72,129.86 -1115.71,165.17 -1082,191 C-1045.71,218.81 -1002.91,227.92 -946,236 C-630.61,280.78 -288.29,300 71,300 C188.01,300 304.89,292.44 419,287 C585.2,279.08 754.78,261.17 912,240 C964.06,232.99 1024.03,228.96 1065,212 C1142.61,179.87 1208.86,90.77 1181,-21 C1169.65,-66.53 1143.86,-103.88 1110,-130 C1077.04,-155.43 1028.35,-175.79 970,-171c "
+                    android:valueTo="M1139 -198 C1112.96,-195.96 1086.75,-190.39 1060,-186 C724.42,-130.9 368.62,-100 -21,-100 C-206.52,-100 -352.82,-106.58 -538,-122 C-587.36,-126.11 -643.08,-130.48 -702,-137 C-729.04,-139.99 -755.08,-146.24 -784,-145 C-814,-143.72 -835.74,-137.28 -857,-128 C-930.63,-95.87 -1001.24,-8.83 -971,105 C-958.3,152.82 -933.55,187.66 -900,214 C-863.65,242.54 -822.66,251.49 -764,258 C-604.41,275.72 -434.69,289.25 -263,295 C-87.78,300.87 91.29,299.55 266,295 C381.34,291.99 494.86,283.26 604,274 C762.94,260.51 929.9,239.45 1083,215 C1134.03,206.85 1192.04,202.57 1233,185 C1309.97,151.99 1375.09,64.69 1348,-47 C1336.52,-94.34 1310.62,-129.78 1277,-156 C1244.09,-181.67 1197.01,-202.54 1139,-198c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="583"
+                    android:valueFrom="M1139 -198 C1112.96,-195.96 1086.75,-190.39 1060,-186 C724.42,-130.9 368.62,-100 -21,-100 C-206.52,-100 -352.82,-106.58 -538,-122 C-587.36,-126.11 -643.08,-130.48 -702,-137 C-729.04,-139.99 -755.08,-146.24 -784,-145 C-814,-143.72 -835.74,-137.28 -857,-128 C-930.63,-95.87 -1001.24,-8.83 -971,105 C-958.3,152.82 -933.55,187.66 -900,214 C-863.65,242.54 -822.66,251.49 -764,258 C-604.41,275.72 -434.69,289.25 -263,295 C-87.78,300.87 91.29,299.55 266,295 C381.34,291.99 494.86,283.26 604,274 C762.94,260.51 929.9,239.45 1083,215 C1134.03,206.85 1192.04,202.57 1233,185 C1309.97,151.99 1375.09,64.69 1348,-47 C1336.52,-94.34 1310.62,-129.78 1277,-156 C1244.09,-181.67 1197.01,-202.54 1139,-198c "
+                    android:valueTo="M1273 -223 C1260.98,-222.01 1248.39,-219.92 1235.6,-217.5 C1222.81,-215.07 1209.81,-212.32 1197,-210 C966.02,-168.25 716.41,-134.99 456.35,-116.17 C196.29,-97.35 -74.22,-92.98 -347,-109 C-371.45,-110.44 -398.42,-112.07 -426.29,-113.9 C-454.16,-115.73 -482.93,-117.76 -511,-120 C-538.24,-122.17 -566.8,-124.89 -593,-121 C-620.44,-116.92 -639.32,-109.31 -659,-99 C-728.07,-62.79 -787.77,29.83 -754,136 C-739.63,181.19 -715.06,213.22 -681,238 C-645.58,263.77 -604.66,274.8 -547,279 C-330.87,294.73 -102.21,301.44 131,299 C529.86,294.82 898.81,248.97 1245,188 C1293.44,179.47 1348.93,172.3 1385,153 C1454.2,115.97 1514.13,26.39 1482,-82 C1468.82,-126.48 1443.86,-160.23 1410,-185 C1376.3,-209.65 1329.97,-227.68 1273,-223c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="600"
+                    android:valueFrom="M1273 -223 C1260.98,-222.01 1248.39,-219.92 1235.6,-217.5 C1222.81,-215.07 1209.81,-212.32 1197,-210 C966.02,-168.25 716.41,-134.99 456.35,-116.17 C196.29,-97.35 -74.22,-92.98 -347,-109 C-371.45,-110.44 -398.42,-112.07 -426.29,-113.9 C-454.16,-115.73 -482.93,-117.76 -511,-120 C-538.24,-122.17 -566.8,-124.89 -593,-121 C-620.44,-116.92 -639.32,-109.31 -659,-99 C-728.07,-62.79 -787.77,29.83 -754,136 C-739.63,181.19 -715.06,213.22 -681,238 C-645.58,263.77 -604.66,274.8 -547,279 C-330.87,294.73 -102.21,301.44 131,299 C529.86,294.82 898.81,248.97 1245,188 C1293.44,179.47 1348.93,172.3 1385,153 C1454.2,115.97 1514.13,26.39 1482,-82 C1468.82,-126.48 1443.86,-160.23 1410,-185 C1376.3,-209.65 1329.97,-227.68 1273,-223c "
+                    android:valueTo="M1396 -247 C1384.29,-246.25 1372.12,-244.54 1359.84,-242.37 C1347.55,-240.21 1335.16,-237.59 1323,-235 C1242.35,-217.84 1159.48,-202.2 1075.58,-188.17 C991.68,-174.14 906.76,-161.72 822,-151 C647.45,-128.92 465.42,-113.12 278.92,-105.12 C92.42,-97.11 -98.55,-96.91 -291,-106 C-319.6,-107.35 -344.62,-105.15 -366.73,-99.83 C-388.84,-94.52 -408.04,-86.09 -425,-75 C-456,-54.72 -484.76,-22.13 -501.39,18.1 C-518.02,58.32 -522.52,106.19 -505,157 C-491.11,197.3 -467.19,230.65 -435,254 C-401.08,278.61 -361.34,290.67 -306,293 C-92.09,302.01 134.66,300.83 346,291 C720.7,273.57 1058.02,226.02 1386,161 C1433.15,151.65 1480.38,141.86 1513,121 C1574.49,81.68 1629.52,-8.18 1595,-111 C1569.5,-186.95 1497.99,-253.57 1396,-247c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="617"
+                    android:valueFrom="M1396 -247 C1384.29,-246.25 1372.12,-244.54 1359.84,-242.37 C1347.55,-240.21 1335.16,-237.59 1323,-235 C1242.35,-217.84 1159.48,-202.2 1075.58,-188.17 C991.68,-174.14 906.76,-161.72 822,-151 C647.45,-128.92 465.42,-113.12 278.92,-105.12 C92.42,-97.11 -98.55,-96.91 -291,-106 C-319.6,-107.35 -344.62,-105.15 -366.73,-99.83 C-388.84,-94.52 -408.04,-86.09 -425,-75 C-456,-54.72 -484.76,-22.13 -501.39,18.1 C-518.02,58.32 -522.52,106.19 -505,157 C-491.11,197.3 -467.19,230.65 -435,254 C-401.08,278.61 -361.34,290.67 -306,293 C-92.09,302.01 134.66,300.83 346,291 C720.7,273.57 1058.02,226.02 1386,161 C1433.15,151.65 1480.38,141.86 1513,121 C1574.49,81.68 1629.52,-8.18 1595,-111 C1569.5,-186.95 1497.99,-253.57 1396,-247c "
+                    android:valueTo="M1487 -268 C1475.75,-267.12 1464.42,-265.34 1453.07,-263.17 C1441.72,-261 1430.34,-258.44 1419,-256 C1408.2,-253.68 1397.48,-251.27 1386.82,-248.89 C1376.16,-246.52 1365.56,-244.17 1355,-242 C1158.69,-201.57 958.13,-168.1 748.31,-143.85 C538.48,-119.59 319.39,-104.56 86,-101 C64.01,-100.66 37.31,-101.63 10.2,-101.81 C-16.9,-102 -44.41,-101.42 -68,-98 C-92.39,-94.46 -112.22,-87.84 -129.29,-79.04 C-146.36,-70.24 -160.66,-59.26 -174,-47 C-201.16,-22.03 -221.68,10.56 -232,51 C-243.2,94.93 -237.31,146.32 -220,184 C-188.2,253.2 -124.98,300 -22,300 C554.96,300 1059.34,235.05 1531,130 C1615.03,111.29 1675.73,61.99 1696,-18 C1707.22,-62.26 1701.41,-113.05 1684,-151 C1653.53,-217.42 1586.39,-275.77 1487,-268c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="633"
+                    android:valueFrom="M1487 -268 C1475.75,-267.12 1464.42,-265.34 1453.07,-263.17 C1441.72,-261 1430.34,-258.44 1419,-256 C1408.2,-253.68 1397.48,-251.27 1386.82,-248.89 C1376.16,-246.52 1365.56,-244.17 1355,-242 C1158.69,-201.57 958.13,-168.1 748.31,-143.85 C538.48,-119.59 319.39,-104.56 86,-101 C64.01,-100.66 37.31,-101.63 10.2,-101.81 C-16.9,-102 -44.41,-101.42 -68,-98 C-92.39,-94.46 -112.22,-87.84 -129.29,-79.04 C-146.36,-70.24 -160.66,-59.26 -174,-47 C-201.16,-22.03 -221.68,10.56 -232,51 C-243.2,94.93 -237.31,146.32 -220,184 C-188.2,253.2 -124.98,300 -22,300 C554.96,300 1059.34,235.05 1531,130 C1615.03,111.29 1675.73,61.99 1696,-18 C1707.22,-62.26 1701.41,-113.05 1684,-151 C1653.53,-217.42 1586.39,-275.77 1487,-268c "
+                    android:valueTo="M1580 -288 C1569.02,-287.82 1558.33,-286.59 1547.73,-284.76 C1537.13,-282.94 1526.62,-280.53 1516,-278 C1329.96,-233.65 1137.53,-196.27 937.45,-167.41 C737.37,-138.56 529.64,-118.24 313,-108 C289.85,-106.91 266.93,-106.34 245.25,-104.34 C223.58,-102.33 203.15,-98.87 185,-92 C151.24,-79.21 121.91,-58.96 99.69,-31.9 C77.47,-4.85 62.35,29.01 57,69 C54.11,90.57 54.97,112.34 58.75,132.84 C62.53,153.33 69.22,172.54 78,189 C109.79,248.58 169.07,295 261,295 C352.67,295 445.29,286.74 535,280 C894.48,253 1223.58,200.63 1539,128 C1578.68,118.86 1619.85,111.36 1654,99 C1720.76,74.84 1771.83,17.2 1783,-61 C1789.05,-103.34 1779.11,-148.02 1762,-181 C1731.69,-239.43 1668.35,-289.44 1580,-288c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="650"
+                    android:valueFrom="M1580 -288 C1569.02,-287.82 1558.33,-286.59 1547.73,-284.76 C1537.13,-282.94 1526.62,-280.53 1516,-278 C1329.96,-233.65 1137.53,-196.27 937.45,-167.41 C737.37,-138.56 529.64,-118.24 313,-108 C289.85,-106.91 266.93,-106.34 245.25,-104.34 C223.58,-102.33 203.15,-98.87 185,-92 C151.24,-79.21 121.91,-58.96 99.69,-31.9 C77.47,-4.85 62.35,29.01 57,69 C54.11,90.57 54.97,112.34 58.75,132.84 C62.53,153.33 69.22,172.54 78,189 C109.79,248.58 169.07,295 261,295 C352.67,295 445.29,286.74 535,280 C894.48,253 1223.58,200.63 1539,128 C1578.68,118.86 1619.85,111.36 1654,99 C1720.76,74.84 1771.83,17.2 1783,-61 C1789.05,-103.34 1779.11,-148.02 1762,-181 C1731.69,-239.43 1668.35,-289.44 1580,-288c "
+                    android:valueTo="M1640 -305 C1621.47,-303.55 1603.27,-299.67 1585.17,-295.09 C1567.08,-290.52 1549.09,-285.24 1531,-281 C1477.75,-268.52 1423.74,-256.18 1369.55,-244.5 C1315.36,-232.82 1260.99,-221.81 1207,-212 C1111.62,-194.67 1018.32,-179.75 923.77,-166.49 C829.22,-153.24 733.41,-141.65 633,-131 C612.82,-128.86 592.3,-127.43 572.08,-125.88 C551.87,-124.33 531.96,-122.65 513,-120 C438.98,-109.65 388.59,-64.25 362,-9 C329.07,59.43 340.86,143.66 378,195 C413.52,244.1 470.86,281.96 558,278 C637.88,274.37 717.93,263.28 796,254 C1110.58,216.59 1403.92,162.28 1683,93 C1720.1,83.79 1748.74,73.39 1775,54 C1800,35.54 1820.18,12.52 1835,-17 C1869.4,-85.51 1855.22,-170.32 1819,-221 C1784.97,-268.62 1722.8,-311.47 1640,-305c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="667"
+                    android:valueFrom="M1640 -305 C1621.47,-303.55 1603.27,-299.67 1585.17,-295.09 C1567.08,-290.52 1549.09,-285.24 1531,-281 C1477.75,-268.52 1423.74,-256.18 1369.55,-244.5 C1315.36,-232.82 1260.99,-221.81 1207,-212 C1111.62,-194.67 1018.32,-179.75 923.77,-166.49 C829.22,-153.24 733.41,-141.65 633,-131 C612.82,-128.86 592.3,-127.43 572.08,-125.88 C551.87,-124.33 531.96,-122.65 513,-120 C438.98,-109.65 388.59,-64.25 362,-9 C329.07,59.43 340.86,143.66 378,195 C413.52,244.1 470.86,281.96 558,278 C637.88,274.37 717.93,263.28 796,254 C1110.58,216.59 1403.92,162.28 1683,93 C1720.1,83.79 1748.74,73.39 1775,54 C1800,35.54 1820.18,12.52 1835,-17 C1869.4,-85.51 1855.22,-170.32 1819,-221 C1784.97,-268.62 1722.8,-311.47 1640,-305c "
+                    android:valueTo="M1701 -321 C1684.31,-319.63 1667.92,-316.14 1651.65,-311.99 C1635.38,-307.84 1619.22,-303.03 1603,-299 C1476.28,-267.54 1347.03,-238.76 1214.58,-213.61 C1082.13,-188.46 946.5,-166.94 807,-150 C789.03,-147.82 771.91,-145.33 755.97,-141.75 C740.03,-138.18 725.27,-133.52 712,-127 C685.66,-114.05 663.18,-96.41 645.39,-74.45 C627.61,-52.5 614.53,-26.22 607,4 C597.72,41.22 599.94,76.18 608.87,106.85 C617.8,137.53 633.44,163.92 651,184 C686.76,224.89 741.19,258.64 818,252 C887.92,245.96 957.59,233.95 1026,224 C1266.28,189.04 1496.64,142.4 1714,85 C1747,76.29 1778.9,70.6 1805,58 C1856.52,33.13 1894.52,-10.44 1911,-72 C1930.34,-144.21 1903.92,-211.1 1869,-252 C1830.94,-296.58 1772.68,-326.88 1701,-321c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="683"
+                    android:valueFrom="M1701 -321 C1684.31,-319.63 1667.92,-316.14 1651.65,-311.99 C1635.38,-307.84 1619.22,-303.03 1603,-299 C1476.28,-267.54 1347.03,-238.76 1214.58,-213.61 C1082.13,-188.46 946.5,-166.94 807,-150 C789.03,-147.82 771.91,-145.33 755.97,-141.75 C740.03,-138.18 725.27,-133.52 712,-127 C685.66,-114.05 663.18,-96.41 645.39,-74.45 C627.61,-52.5 614.53,-26.22 607,4 C597.72,41.22 599.94,76.18 608.87,106.85 C617.8,137.53 633.44,163.92 651,184 C686.76,224.89 741.19,258.64 818,252 C887.92,245.96 957.59,233.95 1026,224 C1266.28,189.04 1496.64,142.4 1714,85 C1747,76.29 1778.9,70.6 1805,58 C1856.52,33.13 1894.52,-10.44 1911,-72 C1930.34,-144.21 1903.92,-211.1 1869,-252 C1830.94,-296.58 1772.68,-326.88 1701,-321c "
+                    android:valueTo="M1762 -336 C1746.19,-334.9 1731.2,-332.08 1716.53,-328.57 C1701.87,-325.06 1687.52,-320.86 1673,-317 C1574.29,-290.78 1473.48,-266.39 1370.87,-244.37 C1268.27,-222.35 1163.87,-202.72 1058,-186 C1025.18,-180.82 995.89,-176.47 969.45,-168.96 C943.01,-161.46 919.42,-150.8 898,-133 C879.29,-117.45 862.09,-98.59 849.04,-75.55 C836,-52.51 827.1,-25.29 825,7 C822.78,41.02 828.46,71.1 838.84,97.01 C849.23,122.92 864.34,144.66 881,162 C915.41,197.81 964.38,224.82 1035,222 C1064.82,220.81 1096.18,213.17 1127,208 C1342.04,171.91 1545.18,128.66 1743,78 C1802.17,62.85 1856.59,51.3 1897,19 C1934.73,-11.15 1965.34,-55.38 1971,-120 C1976.73,-185.43 1950.08,-240.04 1916,-276 C1883.45,-310.34 1827.33,-340.53 1762,-336c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="700"
+                    android:valueFrom="M1762 -336 C1746.19,-334.9 1731.2,-332.08 1716.53,-328.57 C1701.87,-325.06 1687.52,-320.86 1673,-317 C1574.29,-290.78 1473.48,-266.39 1370.87,-244.37 C1268.27,-222.35 1163.87,-202.72 1058,-186 C1025.18,-180.82 995.89,-176.47 969.45,-168.96 C943.01,-161.46 919.42,-150.8 898,-133 C879.29,-117.45 862.09,-98.59 849.04,-75.55 C836,-52.51 827.1,-25.29 825,7 C822.78,41.02 828.46,71.1 838.84,97.01 C849.23,122.92 864.34,144.66 881,162 C915.41,197.81 964.38,224.82 1035,222 C1064.82,220.81 1096.18,213.17 1127,208 C1342.04,171.91 1545.18,128.66 1743,78 C1802.17,62.85 1856.59,51.3 1897,19 C1934.73,-11.15 1965.34,-55.38 1971,-120 C1976.73,-185.43 1950.08,-240.04 1916,-276 C1883.45,-310.34 1827.33,-340.53 1762,-336c "
+                    android:valueTo="M1808 -349 C1793.7,-348.01 1780.06,-345.52 1766.69,-342.4 C1753.32,-339.27 1740.22,-335.52 1727,-332 C1713.99,-328.54 1700.95,-324.87 1688.18,-321.27 C1675.41,-317.68 1662.91,-314.16 1651,-311 C1572.35,-290.15 1494.41,-272.11 1415.73,-255.17 C1337.05,-238.24 1257.62,-222.42 1176,-206 C1149.31,-200.63 1126.51,-191.65 1106.78,-179.38 C1087.04,-167.11 1070.39,-151.55 1056,-133 C1027.49,-96.25 1007.2,-47.97 1014,14 C1019.89,67.66 1045.55,110.83 1080,141 C1113.35,170.21 1161.55,194.66 1223,190 C1277.44,185.87 1332.96,171.84 1387,161 C1548.03,128.69 1702.89,88.79 1854,48 C1905.15,34.19 1945.33,13.53 1975,-24 C2002.3,-58.53 2024.95,-108.71 2018,-172 C2012.18,-225.03 1984.89,-270 1952,-299 C1918.54,-328.51 1867.48,-353.12 1808,-349c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="717"
+                    android:valueFrom="M1808 -349 C1793.7,-348.01 1780.06,-345.52 1766.69,-342.4 C1753.32,-339.27 1740.22,-335.52 1727,-332 C1713.99,-328.54 1700.95,-324.87 1688.18,-321.27 C1675.41,-317.68 1662.91,-314.16 1651,-311 C1572.35,-290.15 1494.41,-272.11 1415.73,-255.17 C1337.05,-238.24 1257.62,-222.42 1176,-206 C1149.31,-200.63 1126.51,-191.65 1106.78,-179.38 C1087.04,-167.11 1070.39,-151.55 1056,-133 C1027.49,-96.25 1007.2,-47.97 1014,14 C1019.89,67.66 1045.55,110.83 1080,141 C1113.35,170.21 1161.55,194.66 1223,190 C1277.44,185.87 1332.96,171.84 1387,161 C1548.03,128.69 1702.89,88.79 1854,48 C1905.15,34.19 1945.33,13.53 1975,-24 C2002.3,-58.53 2024.95,-108.71 2018,-172 C2012.18,-225.03 1984.89,-270 1952,-299 C1918.54,-328.51 1867.48,-353.12 1808,-349c "
+                    android:valueTo="M1853 -361 C1826.42,-359.57 1802.2,-354.21 1778.74,-347.63 C1755.28,-341.06 1732.57,-333.27 1709,-327 C1663.45,-314.88 1616.15,-302.72 1568.63,-290.92 C1521.12,-279.12 1473.39,-267.68 1427,-257 C1404.39,-251.79 1378.86,-247.68 1354,-242.54 C1329.14,-237.41 1304.95,-231.27 1285,-222 C1248.81,-205.19 1214.39,-175.2 1192.48,-135.53 C1170.57,-95.86 1161.16,-46.51 1175,9 C1186.23,54.06 1211.95,90.39 1244,116 C1275.98,141.56 1320.49,162.26 1377,160 C1401.44,159.02 1426.8,152.54 1452,147 C1573.59,120.28 1692.02,92.2 1806,61 C1851.55,48.53 1902.21,39.28 1943,21 C1981.01,3.96 2014.53,-25.54 2035,-63 C2054.98,-99.57 2068.68,-155.45 2055,-210 C2043.49,-255.87 2017.77,-291.27 1986,-317 C1955.82,-341.45 1906.33,-363.87 1853,-361c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="733"
+                    android:valueFrom="M1853 -361 C1826.42,-359.57 1802.2,-354.21 1778.74,-347.63 C1755.28,-341.06 1732.57,-333.27 1709,-327 C1663.45,-314.88 1616.15,-302.72 1568.63,-290.92 C1521.12,-279.12 1473.39,-267.68 1427,-257 C1404.39,-251.79 1378.86,-247.68 1354,-242.54 C1329.14,-237.41 1304.95,-231.27 1285,-222 C1248.81,-205.19 1214.39,-175.2 1192.48,-135.53 C1170.57,-95.86 1161.16,-46.51 1175,9 C1186.23,54.06 1211.95,90.39 1244,116 C1275.98,141.56 1320.49,162.26 1377,160 C1401.44,159.02 1426.8,152.54 1452,147 C1573.59,120.28 1692.02,92.2 1806,61 C1851.55,48.53 1902.21,39.28 1943,21 C1981.01,3.96 2014.53,-25.54 2035,-63 C2054.98,-99.57 2068.68,-155.45 2055,-210 C2043.49,-255.87 2017.77,-291.27 1986,-317 C1955.82,-341.45 1906.33,-363.87 1853,-361c "
+                    android:valueTo="M1879 -371 C1855.84,-369.1 1833.88,-363.8 1812.31,-357.5 C1790.74,-351.2 1769.57,-343.9 1748,-338 C1705.55,-326.38 1662.66,-314.62 1619.58,-303.26 C1576.5,-291.91 1533.22,-280.97 1490,-271 C1477.96,-268.22 1466.77,-265.89 1456.19,-263.3 C1445.61,-260.7 1435.63,-257.84 1426,-254 C1397.93,-242.8 1372.88,-225.68 1352.62,-204.17 C1332.36,-182.66 1316.9,-156.75 1308,-128 C1301.29,-106.31 1298.77,-82.16 1300.06,-58.26 C1301.35,-34.35 1306.46,-10.7 1315,10 C1328.97,43.86 1354.44,76.47 1388.92,99.44 C1423.41,122.41 1466.93,135.73 1517,131 C1538.99,128.93 1561.43,122.33 1584,117 C1694.61,90.86 1801.5,62.81 1906,33 C1950.61,20.28 1990.32,9.42 2021,-15 C2077.17,-59.7 2121.6,-152.41 2082,-249 C2052.82,-320.16 1979.29,-379.23 1879,-371c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="750"
+                    android:valueFrom="M1879 -371 C1855.84,-369.1 1833.88,-363.8 1812.31,-357.5 C1790.74,-351.2 1769.57,-343.9 1748,-338 C1705.55,-326.38 1662.66,-314.62 1619.58,-303.26 C1576.5,-291.91 1533.22,-280.97 1490,-271 C1477.96,-268.22 1466.77,-265.89 1456.19,-263.3 C1445.61,-260.7 1435.63,-257.84 1426,-254 C1397.93,-242.8 1372.88,-225.68 1352.62,-204.17 C1332.36,-182.66 1316.9,-156.75 1308,-128 C1301.29,-106.31 1298.77,-82.16 1300.06,-58.26 C1301.35,-34.35 1306.46,-10.7 1315,10 C1328.97,43.86 1354.44,76.47 1388.92,99.44 C1423.41,122.41 1466.93,135.73 1517,131 C1538.99,128.93 1561.43,122.33 1584,117 C1694.61,90.86 1801.5,62.81 1906,33 C1950.61,20.28 1990.32,9.42 2021,-15 C2077.17,-59.7 2121.6,-152.41 2082,-249 C2052.82,-320.16 1979.29,-379.23 1879,-371c "
+                    android:valueTo="M1917 -381 C1894.47,-379.33 1874.18,-374.77 1854.43,-369.16 C1834.67,-363.54 1815.44,-356.87 1795,-351 C1775.41,-345.37 1755.69,-339.8 1735.98,-334.3 C1716.28,-328.8 1696.57,-323.37 1677,-318 C1655.71,-312.16 1635.71,-307.53 1615.91,-302.93 C1596.11,-298.33 1576.51,-293.75 1556,-288 C1518.95,-277.61 1486.57,-257.44 1461.65,-230.32 C1436.73,-203.2 1419.25,-169.14 1412,-131 C1407.72,-108.49 1407.85,-85.43 1411.23,-63.65 C1414.61,-41.87 1421.25,-21.38 1430,-4 C1445.1,25.99 1469.59,55.09 1501.94,75.77 C1534.28,96.45 1574.48,108.71 1621,105 C1662.49,101.69 1703.3,88.68 1744,78 C1824.94,56.76 1901.6,35.95 1980,12 C2055.14,-10.95 2109.6,-63.03 2125,-144 C2134.11,-191.88 2123.42,-237.96 2108,-270 C2078.07,-332.19 2008.91,-387.8 1917,-381c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="767"
+                    android:valueFrom="M1917 -381 C1894.47,-379.33 1874.18,-374.77 1854.43,-369.16 C1834.67,-363.54 1815.44,-356.87 1795,-351 C1775.41,-345.37 1755.69,-339.8 1735.98,-334.3 C1716.28,-328.8 1696.57,-323.37 1677,-318 C1655.71,-312.16 1635.71,-307.53 1615.91,-302.93 C1596.11,-298.33 1576.51,-293.75 1556,-288 C1518.95,-277.61 1486.57,-257.44 1461.65,-230.32 C1436.73,-203.2 1419.25,-169.14 1412,-131 C1407.72,-108.49 1407.85,-85.43 1411.23,-63.65 C1414.61,-41.87 1421.25,-21.38 1430,-4 C1445.1,25.99 1469.59,55.09 1501.94,75.77 C1534.28,96.45 1574.48,108.71 1621,105 C1662.49,101.69 1703.3,88.68 1744,78 C1824.94,56.76 1901.6,35.95 1980,12 C2055.14,-10.95 2109.6,-63.03 2125,-144 C2134.11,-191.88 2123.42,-237.96 2108,-270 C2078.07,-332.19 2008.91,-387.8 1917,-381c "
+                    android:valueTo="M1938 -389 C1918.02,-387.29 1899.13,-382.71 1880.56,-377.22 C1861.99,-371.74 1843.73,-365.34 1825,-360 C1807.04,-354.88 1788.76,-349.64 1770.36,-344.43 C1751.95,-339.21 1733.43,-334.02 1715,-329 C1695.02,-323.55 1676.29,-319.2 1658.55,-314.13 C1640.81,-309.06 1624.04,-303.29 1608,-295 C1579.39,-280.22 1552.9,-257.61 1533.32,-228.25 C1513.73,-198.88 1501.03,-162.77 1500,-121 C1499.48,-99.88 1502.66,-79.69 1508.25,-61.13 C1513.84,-42.57 1521.86,-25.63 1531,-11 C1565.04,43.45 1626.35,88.61 1719,81 C1756.71,77.9 1795.59,64.05 1832,54 C1869.38,43.69 1905.63,33.72 1942,23 C1979,12.1 2017.29,3.77 2048,-12 C2106.27,-41.91 2155.33,-100.64 2157,-185 C2157.86,-228.59 2143.96,-268.45 2127,-296 C2092.42,-352.19 2025.63,-396.5 1938,-389c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="783"
+                    android:valueFrom="M1938 -389 C1918.02,-387.29 1899.13,-382.71 1880.56,-377.22 C1861.99,-371.74 1843.73,-365.34 1825,-360 C1807.04,-354.88 1788.76,-349.64 1770.36,-344.43 C1751.95,-339.21 1733.43,-334.02 1715,-329 C1695.02,-323.55 1676.29,-319.2 1658.55,-314.13 C1640.81,-309.06 1624.04,-303.29 1608,-295 C1579.39,-280.22 1552.9,-257.61 1533.32,-228.25 C1513.73,-198.88 1501.03,-162.77 1500,-121 C1499.48,-99.88 1502.66,-79.69 1508.25,-61.13 C1513.84,-42.57 1521.86,-25.63 1531,-11 C1565.04,43.45 1626.35,88.61 1719,81 C1756.71,77.9 1795.59,64.05 1832,54 C1869.38,43.69 1905.63,33.72 1942,23 C1979,12.1 2017.29,3.77 2048,-12 C2106.27,-41.91 2155.33,-100.64 2157,-185 C2157.86,-228.59 2143.96,-268.45 2127,-296 C2092.42,-352.19 2025.63,-396.5 1938,-389c "
+                    android:valueTo="M1968 -397 C1948.07,-395.44 1929.72,-391.19 1912.16,-386.08 C1894.59,-380.97 1877.8,-375 1861,-370 C1843.57,-364.82 1826.22,-359.67 1809.03,-354.64 C1791.83,-349.62 1774.8,-344.71 1758,-340 C1738.06,-334.41 1721.02,-329.65 1705.61,-323.91 C1690.2,-318.18 1676.43,-311.48 1663,-302 C1651,-293.53 1639.65,-284.24 1629.09,-272.96 C1618.54,-261.69 1608.79,-248.42 1600,-232 C1582.06,-198.48 1576.15,-159.81 1579.3,-122.98 C1582.45,-86.15 1594.66,-51.16 1613,-25 C1648.02,24.96 1705.28,67.84 1791,61 C1828.15,58.04 1863.89,45.73 1898,36 C1932.04,26.29 1967.24,16.14 2001,6 C2034.31,-4 2068.94,-15.13 2095,-33 C2143.84,-66.49 2187.78,-128.14 2182,-212 C2179.47,-248.64 2164.22,-286.95 2148,-311 C2114.12,-361.25 2048.55,-403.3 1968,-397c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="800"
+                    android:valueFrom="M1968 -397 C1948.07,-395.44 1929.72,-391.19 1912.16,-386.08 C1894.59,-380.97 1877.8,-375 1861,-370 C1843.57,-364.82 1826.22,-359.67 1809.03,-354.64 C1791.83,-349.62 1774.8,-344.71 1758,-340 C1738.06,-334.41 1721.02,-329.65 1705.61,-323.91 C1690.2,-318.18 1676.43,-311.48 1663,-302 C1651,-293.53 1639.65,-284.24 1629.09,-272.96 C1618.54,-261.69 1608.79,-248.42 1600,-232 C1582.06,-198.48 1576.15,-159.81 1579.3,-122.98 C1582.45,-86.15 1594.66,-51.16 1613,-25 C1648.02,24.96 1705.28,67.84 1791,61 C1828.15,58.04 1863.89,45.73 1898,36 C1932.04,26.29 1967.24,16.14 2001,6 C2034.31,-4 2068.94,-15.13 2095,-33 C2143.84,-66.49 2187.78,-128.14 2182,-212 C2179.47,-248.64 2164.22,-286.95 2148,-311 C2114.12,-361.25 2048.55,-403.3 1968,-397c "
+                    android:valueTo="M1994 -404 C1974.92,-402.77 1957.48,-399.23 1940.77,-394.77 C1924.07,-390.3 1908.1,-384.92 1892,-380 C1875.77,-375.04 1859.61,-370.12 1843.46,-365.27 C1827.3,-360.42 1811.17,-355.65 1795,-351 C1762.37,-341.62 1734.85,-328.32 1712.13,-309.52 C1689.4,-290.71 1671.47,-266.4 1658,-235 C1643.1,-200.25 1640.2,-162.74 1645.36,-128.05 C1650.51,-93.35 1663.71,-61.48 1681,-38 C1697.9,-15.06 1720.48,6.55 1748.8,21.74 C1777.12,36.94 1811.17,45.72 1851,43 C1869.38,41.75 1886.68,38.15 1903.48,33.67 C1920.29,29.18 1936.6,23.81 1953,19 C1986.59,9.16 2019.21,-0.58 2050,-10 C2116.98,-30.5 2161.4,-66.4 2187,-126 C2200.41,-157.22 2207.11,-195.75 2202,-233 C2196.75,-271.26 2182.39,-298.2 2165,-322 C2131.47,-367.9 2073.87,-409.14 1994,-404c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="817"
+                    android:valueFrom="M1994 -404 C1974.92,-402.77 1957.48,-399.23 1940.77,-394.77 C1924.07,-390.3 1908.1,-384.92 1892,-380 C1875.77,-375.04 1859.61,-370.12 1843.46,-365.27 C1827.3,-360.42 1811.17,-355.65 1795,-351 C1762.37,-341.62 1734.85,-328.32 1712.13,-309.52 C1689.4,-290.71 1671.47,-266.4 1658,-235 C1643.1,-200.25 1640.2,-162.74 1645.36,-128.05 C1650.51,-93.35 1663.71,-61.48 1681,-38 C1697.9,-15.06 1720.48,6.55 1748.8,21.74 C1777.12,36.94 1811.17,45.72 1851,43 C1869.38,41.75 1886.68,38.15 1903.48,33.67 C1920.29,29.18 1936.6,23.81 1953,19 C1986.59,9.16 2019.21,-0.58 2050,-10 C2116.98,-30.5 2161.4,-66.4 2187,-126 C2200.41,-157.22 2207.11,-195.75 2202,-233 C2196.75,-271.26 2182.39,-298.2 2165,-322 C2131.47,-367.9 2073.87,-409.14 1994,-404c "
+                    android:valueTo="M2013 -410 C2003.61,-409.39 1994.32,-408.36 1985.41,-406.87 C1976.49,-405.39 1967.94,-403.44 1960,-401 C1936.86,-393.89 1913.03,-387.2 1889.78,-380.25 C1866.53,-373.31 1843.85,-366.11 1823,-358 C1795.57,-347.33 1771.25,-331.83 1751.32,-311.26 C1731.4,-290.68 1715.87,-265.02 1706,-234 C1694.29,-197.19 1694.75,-160.83 1702.28,-128.48 C1709.8,-96.13 1724.41,-67.79 1741,-47 C1758.18,-25.47 1780.08,-5.59 1807.49,8.35 C1834.89,22.29 1867.8,30.27 1907,28 C1923.88,27.02 1940.1,23.7 1956.12,19.41 C1972.14,15.12 1987.95,9.86 2004,5 C2019.65,0.27 2035.34,-4.2 2050.65,-8.88 C2065.96,-13.55 2080.89,-18.44 2095,-24 C2153.39,-47.02 2192.43,-90.22 2212,-148 C2235.67,-217.89 2212.62,-293.74 2180,-335 C2149.08,-374.11 2090.43,-414.98 2013,-410c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="833"
+                    android:valueFrom="M2013 -410 C2003.61,-409.39 1994.32,-408.36 1985.41,-406.87 C1976.49,-405.39 1967.94,-403.44 1960,-401 C1936.86,-393.89 1913.03,-387.2 1889.78,-380.25 C1866.53,-373.31 1843.85,-366.11 1823,-358 C1795.57,-347.33 1771.25,-331.83 1751.32,-311.26 C1731.4,-290.68 1715.87,-265.02 1706,-234 C1694.29,-197.19 1694.75,-160.83 1702.28,-128.48 C1709.8,-96.13 1724.41,-67.79 1741,-47 C1758.18,-25.47 1780.08,-5.59 1807.49,8.35 C1834.89,22.29 1867.8,30.27 1907,28 C1923.88,27.02 1940.1,23.7 1956.12,19.41 C1972.14,15.12 1987.95,9.86 2004,5 C2019.65,0.27 2035.34,-4.2 2050.65,-8.88 C2065.96,-13.55 2080.89,-18.44 2095,-24 C2153.39,-47.02 2192.43,-90.22 2212,-148 C2235.67,-217.89 2212.62,-293.74 2180,-335 C2149.08,-374.11 2090.43,-414.98 2013,-410c "
+                    android:valueTo="M2026 -415 C2008.21,-413.68 1992.19,-410.31 1976.88,-406.07 C1961.57,-401.83 1946.96,-396.74 1932,-392 C1916.81,-387.18 1901.5,-382.75 1887.03,-377.86 C1872.55,-372.97 1858.89,-367.63 1847,-361 C1823.18,-347.72 1801.58,-330.64 1784.24,-309.04 C1766.9,-287.45 1753.81,-261.34 1747,-230 C1738.93,-192.85 1741.7,-158.46 1750.72,-128.55 C1759.75,-98.64 1775.03,-73.21 1792,-54 C1809.62,-34.04 1831.33,-15.84 1858.28,-3.23 C1885.23,9.38 1917.42,16.4 1956,14 C1972.29,12.99 1987.98,9.55 2003.26,5.22 C2018.53,0.88 2033.39,-4.37 2048,-9 C2062.62,-13.63 2077.58,-18.1 2092.03,-23.05 C2106.48,-28 2120.42,-33.44 2133,-40 C2183.73,-66.48 2220.06,-111.51 2234,-170 C2251.17,-242.03 2221.03,-311.29 2190,-347 C2155.44,-386.78 2098.94,-420.39 2026,-415c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="850"
+                    android:valueFrom="M2026 -415 C2008.21,-413.68 1992.19,-410.31 1976.88,-406.07 C1961.57,-401.83 1946.96,-396.74 1932,-392 C1916.81,-387.18 1901.5,-382.75 1887.03,-377.86 C1872.55,-372.97 1858.89,-367.63 1847,-361 C1823.18,-347.72 1801.58,-330.64 1784.24,-309.04 C1766.9,-287.45 1753.81,-261.34 1747,-230 C1738.93,-192.85 1741.7,-158.46 1750.72,-128.55 C1759.75,-98.64 1775.03,-73.21 1792,-54 C1809.62,-34.04 1831.33,-15.84 1858.28,-3.23 C1885.23,9.38 1917.42,16.4 1956,14 C1972.29,12.99 1987.98,9.55 2003.26,5.22 C2018.53,0.88 2033.39,-4.37 2048,-9 C2062.62,-13.63 2077.58,-18.1 2092.03,-23.05 C2106.48,-28 2120.42,-33.44 2133,-40 C2183.73,-66.48 2220.06,-111.51 2234,-170 C2251.17,-242.03 2221.03,-311.29 2190,-347 C2155.44,-386.78 2098.94,-420.39 2026,-415c "
+                    android:valueTo="M2035 -419 C2018.03,-417.61 2002.83,-414.17 1988.22,-409.92 C1973.61,-405.68 1959.6,-400.62 1945,-396 C1929.99,-391.24 1915.88,-386.54 1902.83,-381.12 C1889.77,-375.71 1877.78,-369.59 1867,-362 C1846.17,-347.34 1826.91,-328.8 1811.85,-305.82 C1796.8,-282.85 1785.97,-255.42 1782,-223 C1777.51,-186.32 1782.26,-154.15 1792.5,-126.62 C1802.75,-99.08 1818.49,-76.18 1836,-58 C1853.47,-39.86 1875.92,-23.08 1903.02,-11.72 C1930.13,-0.36 1961.89,5.57 1998,2 C2013.61,0.46 2028.33,-2.98 2042.78,-7.18 C2057.24,-11.39 2071.43,-16.37 2086,-21 C2099.99,-25.45 2114.29,-30 2127.82,-35.29 C2141.34,-40.57 2154.1,-46.6 2165,-54 C2209.24,-84.04 2242.69,-131.41 2251,-192 C2261.06,-265.31 2230.86,-322.96 2198,-358 C2164.79,-393.41 2109.99,-425.16 2035,-419c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="867"
+                    android:valueFrom="M2035 -419 C2018.03,-417.61 2002.83,-414.17 1988.22,-409.92 C1973.61,-405.68 1959.6,-400.62 1945,-396 C1929.99,-391.24 1915.88,-386.54 1902.83,-381.12 C1889.77,-375.71 1877.78,-369.59 1867,-362 C1846.17,-347.34 1826.91,-328.8 1811.85,-305.82 C1796.8,-282.85 1785.97,-255.42 1782,-223 C1777.51,-186.32 1782.26,-154.15 1792.5,-126.62 C1802.75,-99.08 1818.49,-76.18 1836,-58 C1853.47,-39.86 1875.92,-23.08 1903.02,-11.72 C1930.13,-0.36 1961.89,5.57 1998,2 C2013.61,0.46 2028.33,-2.98 2042.78,-7.18 C2057.24,-11.39 2071.43,-16.37 2086,-21 C2099.99,-25.45 2114.29,-30 2127.82,-35.29 C2141.34,-40.57 2154.1,-46.6 2165,-54 C2209.24,-84.04 2242.69,-131.41 2251,-192 C2261.06,-265.31 2230.86,-322.96 2198,-358 C2164.79,-393.41 2109.99,-425.16 2035,-419c "
+                    android:valueTo="M2048 -423 C2034.52,-421.89 2023.13,-419.7 2012.3,-416.87 C2001.47,-414.05 1991.22,-410.61 1980,-407 C1969.12,-403.5 1958.34,-400.34 1948.04,-396.88 C1937.74,-393.43 1927.93,-389.67 1919,-385 C1899.26,-374.66 1883.06,-362.52 1869.52,-348.78 C1855.98,-335.04 1845.1,-319.71 1836,-303 C1826.17,-284.95 1818.62,-262.83 1814.8,-239.16 C1810.98,-215.48 1810.9,-190.26 1816,-166 C1824.81,-124.13 1845.68,-87.42 1875.54,-59.91 C1905.39,-32.39 1944.24,-14.08 1989,-9 C2015.48,-5.99 2038.19,-8.51 2059.92,-13.64 C2081.65,-18.77 2102.41,-26.52 2125,-34 C2162.45,-46.39 2197.45,-69.04 2223.09,-101.01 C2248.73,-132.98 2265,-174.29 2265,-224 C2265,-257.55 2257.85,-285.51 2246.41,-309.23 C2234.96,-332.95 2219.21,-352.42 2202,-369 C2168.19,-401.57 2116.15,-428.59 2048,-423c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="883"
+                    android:valueFrom="M2048 -423 C2034.52,-421.89 2023.13,-419.7 2012.3,-416.87 C2001.47,-414.05 1991.22,-410.61 1980,-407 C1969.12,-403.5 1958.34,-400.34 1948.04,-396.88 C1937.74,-393.43 1927.93,-389.67 1919,-385 C1899.26,-374.66 1883.06,-362.52 1869.52,-348.78 C1855.98,-335.04 1845.1,-319.71 1836,-303 C1826.17,-284.95 1818.62,-262.83 1814.8,-239.16 C1810.98,-215.48 1810.9,-190.26 1816,-166 C1824.81,-124.13 1845.68,-87.42 1875.54,-59.91 C1905.39,-32.39 1944.24,-14.08 1989,-9 C2015.48,-5.99 2038.19,-8.51 2059.92,-13.64 C2081.65,-18.77 2102.41,-26.52 2125,-34 C2162.45,-46.39 2197.45,-69.04 2223.09,-101.01 C2248.73,-132.98 2265,-174.29 2265,-224 C2265,-257.55 2257.85,-285.51 2246.41,-309.23 C2234.96,-332.95 2219.21,-352.42 2202,-369 C2168.19,-401.57 2116.15,-428.59 2048,-423c "
+                    android:valueTo="M2056 -426 C2040.73,-424.75 2025.49,-421.51 2010.94,-417.46 C1996.39,-413.4 1982.52,-408.53 1970,-404 C1956.36,-399.06 1944.24,-393.17 1933.34,-386.46 C1922.45,-379.75 1912.77,-372.22 1904,-364 C1886.94,-348 1870.57,-328.74 1858.28,-304.99 C1845.98,-281.24 1837.76,-252.98 1837,-219 C1836.25,-185.71 1842.79,-156.92 1854.12,-132.22 C1865.45,-107.52 1881.57,-86.92 1900,-70 C1917.59,-53.86 1939.31,-38.58 1965.28,-28.23 C1991.24,-17.87 2021.45,-12.45 2056,-16 C2069.73,-17.41 2083.86,-20.52 2098,-24.48 C2112.14,-28.43 2126.27,-33.23 2140,-38 C2177.19,-50.93 2209.98,-73.51 2233.83,-104.56 C2257.68,-135.61 2272.58,-175.15 2274,-222 C2275.03,-256.11 2267.97,-285.17 2256.56,-309.79 C2245.15,-334.42 2229.38,-354.62 2213,-371 C2179.81,-404.19 2126.45,-431.78 2056,-426c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="900"
+                    android:valueFrom="M2056 -426 C2040.73,-424.75 2025.49,-421.51 2010.94,-417.46 C1996.39,-413.4 1982.52,-408.53 1970,-404 C1956.36,-399.06 1944.24,-393.17 1933.34,-386.46 C1922.45,-379.75 1912.77,-372.22 1904,-364 C1886.94,-348 1870.57,-328.74 1858.28,-304.99 C1845.98,-281.24 1837.76,-252.98 1837,-219 C1836.25,-185.71 1842.79,-156.92 1854.12,-132.22 C1865.45,-107.52 1881.57,-86.92 1900,-70 C1917.59,-53.86 1939.31,-38.58 1965.28,-28.23 C1991.24,-17.87 2021.45,-12.45 2056,-16 C2069.73,-17.41 2083.86,-20.52 2098,-24.48 C2112.14,-28.43 2126.27,-33.23 2140,-38 C2177.19,-50.93 2209.98,-73.51 2233.83,-104.56 C2257.68,-135.61 2272.58,-175.15 2274,-222 C2275.03,-256.11 2267.97,-285.17 2256.56,-309.79 C2245.15,-334.42 2229.38,-354.62 2213,-371 C2179.81,-404.19 2126.45,-431.78 2056,-426c "
+                    android:valueTo="M2070 -429 C2054.11,-427.83 2039.39,-424.99 2025.5,-421.24 C2011.62,-417.49 1998.56,-412.83 1986,-408 C1972.35,-402.75 1960.84,-396.75 1950.5,-390.06 C1940.16,-383.37 1930.98,-375.99 1922,-368 C1904.86,-352.74 1888.86,-333.04 1877.15,-309 C1865.43,-284.96 1858,-256.59 1858,-224 C1858,-191.48 1864.22,-163.11 1874.96,-138.68 C1885.69,-114.24 1900.94,-93.75 1919,-77 C1936.46,-60.8 1957.91,-45.91 1983.4,-35.64 C2008.89,-25.36 2038.42,-19.71 2072,-22 C2085.63,-22.93 2099.7,-25.74 2113.57,-29.54 C2127.45,-33.34 2141.13,-38.13 2154,-43 C2188.92,-56.23 2220.88,-79.24 2244.25,-110.32 C2267.62,-141.39 2282.41,-180.53 2283,-226 C2283.44,-259.42 2276.49,-288.29 2265.28,-312.75 C2254.07,-337.2 2238.61,-357.24 2222,-373 C2189.33,-404.01 2137.11,-433.96 2070,-429c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="917"
+                    android:valueFrom="M2070 -429 C2054.11,-427.83 2039.39,-424.99 2025.5,-421.24 C2011.62,-417.49 1998.56,-412.83 1986,-408 C1972.35,-402.75 1960.84,-396.75 1950.5,-390.06 C1940.16,-383.37 1930.98,-375.99 1922,-368 C1904.86,-352.74 1888.86,-333.04 1877.15,-309 C1865.43,-284.96 1858,-256.59 1858,-224 C1858,-191.48 1864.22,-163.11 1874.96,-138.68 C1885.69,-114.24 1900.94,-93.75 1919,-77 C1936.46,-60.8 1957.91,-45.91 1983.4,-35.64 C2008.89,-25.36 2038.42,-19.71 2072,-22 C2085.63,-22.93 2099.7,-25.74 2113.57,-29.54 C2127.45,-33.34 2141.13,-38.13 2154,-43 C2188.92,-56.23 2220.88,-79.24 2244.25,-110.32 C2267.62,-141.39 2282.41,-180.53 2283,-226 C2283.44,-259.42 2276.49,-288.29 2265.28,-312.75 C2254.07,-337.2 2238.61,-357.24 2222,-373 C2189.33,-404.01 2137.11,-433.96 2070,-429c "
+                    android:valueTo="M2074 -431 C2067.27,-430.47 2061.91,-429.95 2057.16,-429.32 C2052.4,-428.69 2048.27,-427.96 2044,-427 C2016.54,-420.84 1992.45,-411.46 1971.52,-399.05 C1950.6,-386.65 1932.83,-371.23 1918,-353 C1905.73,-337.91 1892.68,-315.95 1883.98,-289.37 C1875.27,-262.79 1870.91,-231.58 1876,-198 C1880.07,-171.13 1888.38,-147.32 1900.64,-126.25 C1912.9,-105.19 1929.11,-86.87 1949,-71 C1967.14,-56.52 1990.42,-43.76 2016.89,-35.75 C2043.37,-27.74 2073.05,-24.48 2104,-29 C2131.69,-33.04 2156.62,-41.35 2178.66,-53 C2200.71,-64.65 2219.86,-79.63 2236,-97 C2251.9,-114.12 2266.06,-135.28 2275.66,-159.96 C2285.26,-184.64 2290.3,-212.83 2288,-244 C2285.7,-275.1 2278.29,-301.18 2267.16,-323.44 C2256.03,-345.7 2241.18,-364.14 2224,-380 C2192.75,-408.84 2138.58,-436.05 2074,-431c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="933"
+                    android:valueFrom="M2074 -431 C2067.27,-430.47 2061.91,-429.95 2057.16,-429.32 C2052.4,-428.69 2048.27,-427.96 2044,-427 C2016.54,-420.84 1992.45,-411.46 1971.52,-399.05 C1950.6,-386.65 1932.83,-371.23 1918,-353 C1905.73,-337.91 1892.68,-315.95 1883.98,-289.37 C1875.27,-262.79 1870.91,-231.58 1876,-198 C1880.07,-171.13 1888.38,-147.32 1900.64,-126.25 C1912.9,-105.19 1929.11,-86.87 1949,-71 C1967.14,-56.52 1990.42,-43.76 2016.89,-35.75 C2043.37,-27.74 2073.05,-24.48 2104,-29 C2131.69,-33.04 2156.62,-41.35 2178.66,-53 C2200.71,-64.65 2219.86,-79.63 2236,-97 C2251.9,-114.12 2266.06,-135.28 2275.66,-159.96 C2285.26,-184.64 2290.3,-212.83 2288,-244 C2285.7,-275.1 2278.29,-301.18 2267.16,-323.44 C2256.03,-345.7 2241.18,-364.14 2224,-380 C2192.75,-408.84 2138.58,-436.05 2074,-431c "
+                    android:valueTo="M2090 -433 C2079.15,-432.82 2068.99,-431.84 2059.32,-430.22 C2049.66,-428.59 2040.5,-426.32 2031.67,-423.56 C2022.84,-420.81 2014.34,-417.57 2006,-414 C1972.49,-399.68 1942.32,-376.89 1920.49,-346.2 C1898.66,-315.52 1885.16,-276.93 1885,-231 C1884.89,-199.3 1891.54,-172.16 1902.39,-148.76 C1913.24,-125.35 1928.3,-105.7 1945,-89 C1961.99,-72.01 1982.49,-57.13 2006.61,-46.69 C2030.74,-36.25 2058.5,-30.24 2090,-31 C2118.98,-31.69 2146.38,-38.09 2170.71,-48.49 C2195.04,-58.89 2216.3,-73.3 2233,-90 C2249.92,-106.92 2265.03,-126.77 2275.99,-150.22 C2286.94,-173.67 2293.73,-200.71 2294,-232 C2294.27,-263.44 2287.4,-291.32 2276.52,-315.27 C2265.64,-339.22 2250.75,-359.25 2235,-375 C2218.75,-391.25 2198.15,-406 2173.79,-416.57 C2149.42,-427.13 2121.3,-433.51 2090,-433c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="950"
+                    android:valueFrom="M2090 -433 C2079.15,-432.82 2068.99,-431.84 2059.32,-430.22 C2049.66,-428.59 2040.5,-426.32 2031.67,-423.56 C2022.84,-420.81 2014.34,-417.57 2006,-414 C1972.49,-399.68 1942.32,-376.89 1920.49,-346.2 C1898.66,-315.52 1885.16,-276.93 1885,-231 C1884.89,-199.3 1891.54,-172.16 1902.39,-148.76 C1913.24,-125.35 1928.3,-105.7 1945,-89 C1961.99,-72.01 1982.49,-57.13 2006.61,-46.69 C2030.74,-36.25 2058.5,-30.24 2090,-31 C2118.98,-31.69 2146.38,-38.09 2170.71,-48.49 C2195.04,-58.89 2216.3,-73.3 2233,-90 C2249.92,-106.92 2265.03,-126.77 2275.99,-150.22 C2286.94,-173.67 2293.73,-200.71 2294,-232 C2294.27,-263.44 2287.4,-291.32 2276.52,-315.27 C2265.64,-339.22 2250.75,-359.25 2235,-375 C2218.75,-391.25 2198.15,-406 2173.79,-416.57 C2149.42,-427.13 2121.3,-433.51 2090,-433c "
+                    android:valueTo="M2088 -434 C2073.56,-433.07 2058.65,-430.77 2044.55,-427.34 C2030.45,-423.92 2017.17,-419.39 2006,-414 C1994.71,-408.55 1983.93,-402.05 1974.11,-394.92 C1964.29,-387.78 1955.43,-380.01 1948,-372 C1932.68,-355.48 1918.25,-334.46 1907.99,-309.83 C1897.73,-285.2 1891.64,-256.96 1893,-226 C1894.35,-195.33 1901.36,-168.86 1912.41,-146.04 C1923.45,-123.22 1938.53,-104.06 1956,-88 C1973.16,-72.23 1994.54,-57.77 2019.07,-47.74 C2043.6,-37.7 2071.27,-32.08 2101,-34 C2130.44,-35.9 2156.54,-42.38 2179.6,-52.91 C2202.66,-63.43 2222.69,-77.98 2240,-96 C2256.04,-112.7 2270.94,-132.52 2281.5,-156.25 C2292.07,-179.98 2298.29,-207.63 2297,-240 C2295.78,-270.62 2288.45,-297.7 2277.35,-320.96 C2266.24,-344.22 2251.35,-363.66 2235,-379 C2201.84,-410.09 2151.36,-438.08 2088,-434c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="967"
+                    android:valueFrom="M2088 -434 C2073.56,-433.07 2058.65,-430.77 2044.55,-427.34 C2030.45,-423.92 2017.17,-419.39 2006,-414 C1994.71,-408.55 1983.93,-402.05 1974.11,-394.92 C1964.29,-387.78 1955.43,-380.01 1948,-372 C1932.68,-355.48 1918.25,-334.46 1907.99,-309.83 C1897.73,-285.2 1891.64,-256.96 1893,-226 C1894.35,-195.33 1901.36,-168.86 1912.41,-146.04 C1923.45,-123.22 1938.53,-104.06 1956,-88 C1973.16,-72.23 1994.54,-57.77 2019.07,-47.74 C2043.6,-37.7 2071.27,-32.08 2101,-34 C2130.44,-35.9 2156.54,-42.38 2179.6,-52.91 C2202.66,-63.43 2222.69,-77.98 2240,-96 C2256.04,-112.7 2270.94,-132.52 2281.5,-156.25 C2292.07,-179.98 2298.29,-207.63 2297,-240 C2295.78,-270.62 2288.45,-297.7 2277.35,-320.96 C2266.24,-344.22 2251.35,-363.66 2235,-379 C2201.84,-410.09 2151.36,-438.08 2088,-434c "
+                    android:valueTo="M2081 -434 C2061.85,-432.43 2043.71,-428.03 2026.99,-421.68 C2010.26,-415.32 1994.96,-407.01 1981.49,-397.61 C1968.02,-388.21 1956.39,-377.72 1947,-367 C1932.41,-350.35 1918.99,-329.51 1909.93,-304.5 C1900.87,-279.49 1896.16,-250.32 1899,-217 C1901.47,-187.99 1909.18,-162.6 1920.78,-140.53 C1932.39,-118.47 1947.91,-99.72 1966,-84 C1983.82,-68.52 2006.42,-55.12 2032.02,-46.29 C2057.61,-37.46 2086.2,-33.2 2116,-36 C2145.54,-38.78 2170.61,-46.49 2192.26,-58.02 C2213.91,-69.56 2232.15,-84.92 2248,-103 C2263.25,-120.39 2277.16,-141.16 2286.55,-165.69 C2295.94,-190.23 2300.82,-218.54 2298,-251 C2295.43,-280.64 2287.42,-306.3 2275.82,-328.47 C2264.21,-350.64 2248.99,-369.32 2232,-385 C2215.77,-399.98 2193.85,-413.53 2168.07,-422.69 C2142.29,-431.84 2112.66,-436.6 2081,-434c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="983"
+                    android:pathData="M 50,50C 50,50 50,50 50,50"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="0">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:pathData="M 50,50C 50,50 50,50 50,50"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="983">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_1_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="983"
+                    android:pathData="M 32,12C 32,12 32,12 32,12"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="0">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="17"
+                    android:pathData="M 32,12C 32,12 32,12 32,12"
+                    android:propertyName="translateXY"
+                    android:propertyXName="translateX"
+                    android:propertyYName="translateY"
+                    android:startOffset="983">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1000"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="64dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="64">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_2_G"
+                    android:translateX="32"
+                    android:translateY="12">
+                    <path
+                        android:name="_R_G_L_2_G_D_0_P_0"
+                        android:fillAlpha="0"
+                        android:fillColor="?attr/colorControlNormal"
+                        android:fillType="nonZero"
+                        android:pathData=" M32 -12 C32,-12 32,12 32,12 C32,12 -32,12 -32,12 C-32,12 -32,-12 -32,-12 C-32,-12 32,-12 32,-12c " />
+                </group>
+                <group
+                    android:name="_R_G_L_1_G_N_1_T_1"
+                    android:scaleX="0.01"
+                    android:scaleY="0.01"
+                    android:translateX="32"
+                    android:translateY="12">
+                    <group
+                        android:name="_R_G_L_1_G_N_1_T_0"
+                        android:translateX="-50"
+                        android:translateY="-50">
+                        <group
+                            android:name="_R_G_L_1_G"
+                            android:translateX="50"
+                            android:translateY="50">
+                            <path
+                                android:name="_R_G_L_1_G_D_0_P_0"
+                                android:fillAlpha="0.2"
+                                android:fillColor="?attr/colorProgressBackgroundNormal"
+                                android:fillType="nonZero"
+                                android:pathData=" M2080 -434 C2053.41,-431.82 2032.66,-423.65 2009,-416 C1677.83,-308.94 1328.18,-225.31 948,-169 C552.55,-110.43 82.93,-85.03 -368,-110 C-945.44,-141.98 -1453.99,-244.34 -1920,-388 C-1941.56,-394.65 -1959.08,-402.44 -1986,-409 C-2037.81,-421.62 -2094.99,-409.05 -2131,-389 C-2199,-351.14 -2259.29,-260.99 -2224,-153 C-2209.83,-109.65 -2185.04,-77.07 -2151,-52 C-2117.95,-27.66 -2071.89,-16.07 -2026,-2 C-1671.19,106.76 -1289.85,190.97 -878,245 C-470.97,298.4 -1.15,313.9 448,286 C879.56,259.19 1286.12,192.07 1657,100 C1789.03,67.23 1935.92,26.61 2065,-14 C2109.16,-27.89 2157.33,-39.88 2194,-60 C2261.75,-97.17 2324.86,-186 2290,-295 C2263.92,-376.54 2191.93,-443.19 2080,-434c " />
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_N_1_T_1"
+                    android:scaleX="0.01"
+                    android:scaleY="0.01"
+                    android:translateX="32"
+                    android:translateY="12">
+                    <group
+                        android:name="_R_G_L_0_G_N_1_T_0"
+                        android:translateX="-50"
+                        android:translateY="-50">
+                        <group
+                            android:name="_R_G_L_0_G"
+                            android:translateX="50"
+                            android:translateY="50">
+                            <path
+                                android:name="_R_G_L_0_G_D_0_P_0"
+                                android:fillAlpha="0"
+                                android:fillColor="?attr/colorControlNormal"
+                                android:fillType="nonZero"
+                                android:pathData=" M-2053 -413 C-2064.2,-412.04 -2074.66,-410.29 -2084.48,-407.87 C-2094.29,-405.46 -2103.47,-402.36 -2112.09,-398.71 C-2120.71,-395.06 -2128.79,-390.85 -2136.41,-386.19 C-2144.02,-381.54 -2151.19,-376.43 -2158,-371 C-2168.19,-362.86 -2177.05,-354.51 -2184.8,-345.79 C-2192.55,-337.07 -2199.2,-327.99 -2204.98,-318.41 C-2210.77,-308.83 -2215.7,-298.74 -2220,-288 C-2237.22,-245.04 -2237.06,-202.14 -2226.93,-164.3 C-2216.8,-126.46 -2196.69,-93.69 -2174,-71 C-2150.2,-47.2 -2115.92,-28.38 -2077.59,-19.79 C-2039.27,-11.2 -1996.92,-12.85 -1957,-30 C-1925.35,-43.59 -1896.01,-64.58 -1873.88,-92.68 C-1851.74,-120.77 -1836.82,-155.98 -1834,-198 C-1831.53,-234.86 -1837.92,-266 -1849.1,-292.1 C-1860.29,-318.21 -1876.28,-339.28 -1893,-356 C-1910.36,-373.36 -1932.3,-389.15 -1958.94,-399.84 C-1985.57,-410.52 -2016.89,-416.09 -2053,-413c " />
+                        </group>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/core/res/res/drawable-watch/progress_horizontal_material.xml b/core/res/res/drawable-watch/progress_horizontal_material.xml
new file mode 100644
index 0000000..8c52a41
--- /dev/null
+++ b/core/res/res/drawable-watch/progress_horizontal_material.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@id/background"
+          android:gravity="center_vertical|fill_horizontal">
+        <shape android:shape="rectangle">
+            <corners android:radius="?attr/progressBarCornerRadius" />
+            <size android:height="@dimen/progress_bar_height_material" />
+            <solid android:color="@color/material_grey_900" />
+        </shape>
+    </item>
+    <item android:id="@id/secondaryProgress"
+          android:gravity="center_vertical|fill_horizontal">
+        <scale android:scaleWidth="100%">
+            <shape android:shape="rectangle">
+                <corners android:radius="?attr/progressBarCornerRadius" />
+                <size android:height="@dimen/progress_bar_height_material" />
+                <solid android:color="@color/material_grey_900" />
+            </shape>
+        </scale>
+    </item>
+    <item android:id="@id/progress"
+          android:gravity="center_vertical|fill_horizontal">
+        <scale android:scaleWidth="100%">
+            <shape android:shape="rectangle">
+                <corners android:radius="?attr/progressBarCornerRadius" />
+                <size android:height="@dimen/progress_bar_height_material" />
+                <solid android:color="@color/white" />
+            </shape>
+        </scale>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/res/res/values-watch/dimens_material.xml b/core/res/res/values-watch/dimens_material.xml
index 51d4018..40673c1 100644
--- a/core/res/res/values-watch/dimens_material.xml
+++ b/core/res/res/values-watch/dimens_material.xml
@@ -44,6 +44,7 @@
     <dimen name="progress_bar_size_small">16dip</dimen>
     <dimen name="progress_bar_size_medium">32dip</dimen>
     <dimen name="progress_bar_size_large">64dip</dimen>
+    <dimen name="progress_bar_height">24dp</dimen>
 
     <!-- Progress bar message dimens -->
     <dimen name="message_progress_dialog_text_size">18sp</dimen>
diff --git a/core/res/res/values-watch/strings.xml b/core/res/res/values-watch/strings.xml
index 6d6cfc7..9fa4bef 100644
--- a/core/res/res/values-watch/strings.xml
+++ b/core/res/res/values-watch/strings.xml
@@ -26,6 +26,7 @@
     <string name="global_action_emergency">Emergency SOS</string>
 
    <!-- Reboot to Recovery Progress Dialog. This is shown before it reboots to recovery. -->
+    <string name="reboot_to_update_prepare">Preparing to update</string>
     <string name="reboot_to_update_title">Wear OS system update</string>
 
    <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. -->
diff --git a/core/res/res/values-watch/styles_material.xml b/core/res/res/values-watch/styles_material.xml
index 12bc406..8698e86 100644
--- a/core/res/res/values-watch/styles_material.xml
+++ b/core/res/res/values-watch/styles_material.xml
@@ -107,4 +107,14 @@
         <item name="paddingStart">@dimen/message_progress_dialog_start_padding</item>
         <item name="paddingTop">@dimen/message_progress_dialog_top_padding</item>
     </style>
+
+    <!-- Material progress part (indeterminate/horizontal) for Wear -->
+    <style name="Widget.Material.ProgressBar.Horizontal" parent="Widget.ProgressBar.Horizontal">
+        <item name="progressDrawable">@drawable/progress_horizontal_material</item>
+        <item name="indeterminateDrawable">
+            @drawable/progress_indeterminate_horizontal_material
+        </item>
+        <item name="minHeight">@dimen/progress_bar_height</item>
+        <item name="maxHeight">@dimen/progress_bar_height</item>
+    </style>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7602f69..f63f8b3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -198,6 +198,9 @@
          service. Off by default, since the service may not be available on some devices. -->
     <bool name="config_enableProximityService">false</bool>
 
+    <!-- Enable or disable android.companion.virtual.VirtualDeviceManager. Enabled by default. -->
+    <bool name="config_enableVirtualDeviceManager">true</bool>
+
     <!-- Whether dialogs should close automatically when the user touches outside
          of them.  This should not normally be modified. -->
     <bool name="config_closeDialogWhenTouchOutside">true</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 978849d..8e52633 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -418,6 +418,7 @@
   <java-symbol type="bool" name="config_localDisplaysMirrorContent" />
   <java-symbol type="bool" name="config_ignoreUdfpsVote" />
   <java-symbol type="bool" name="config_enableProximityService" />
+  <java-symbol type="bool" name="config_enableVirtualDeviceManager" />
   <java-symbol type="array" name="config_localPrivateDisplayPorts" />
   <java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" />
   <java-symbol type="bool" name="config_enableAppWidgetService" />
diff --git a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
index 91c7687..9677584 100644
--- a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
@@ -19,10 +19,13 @@
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+
 import android.database.sqlite.SQLiteException;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileWriter;
@@ -61,7 +64,6 @@
         assertTrue(mDatabaseFile.exists());
     }
 
-
     public void testDatabaseIsCorrupt() throws IOException {
         mDatabase.execSQL("create table t (i int);");
         // write junk into the database file
@@ -70,30 +72,23 @@
         writer.close();
         assertTrue(mDatabaseFile.exists());
         // since the database file is now corrupt, doing any sql on this database connection
-        // should trigger call to MyDatabaseCorruptionHandler.onCorruption
+        // should trigger call to MyDatabaseCorruptionHandler.onCorruption.  A corruption
+        // exception will also be throws.  This seems redundant.
         try {
             mDatabase.execSQL("select * from t;");
             fail("expected exception");
-        } catch (SQLiteDiskIOException e) {
-            /**
-             * this test used to produce a corrupted db. but with new sqlite it instead reports
-             * Disk I/O error. meh..
-             * need to figure out how to cause corruption in db
-             */
-            // expected
-            if (mDatabaseFile.exists()) {
-                mDatabaseFile.delete();
-            }
-        } catch (SQLiteException e) {
-            
+        } catch (SQLiteDatabaseCorruptException e) {
+            // Expected result.
         }
-        // database file should be gone
+
+        // The database file should be gone.
         assertFalse(mDatabaseFile.exists());
-        // after corruption handler is called, the database file should be free of
-        // database corruption
-        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null,
+        // After corruption handler is called, the database file should be free of
+        // database corruption.   Reopen it.
+        mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null,
                 new MyDatabaseCorruptionHandler());
-        assertTrue(db.isDatabaseIntegrityOk());
+        assertTrue(mDatabase.isDatabaseIntegrityOk());
+        // The teadDown() routine will close the database.
     }
 
     /**
@@ -102,8 +97,21 @@
      * corrupt before deleting the file.
      */
     public class MyDatabaseCorruptionHandler implements DatabaseErrorHandler {
+        private final AtomicBoolean mEntered = new AtomicBoolean(false);
         public void onCorruption(SQLiteDatabase dbObj) {
-            boolean databaseOk = dbObj.isDatabaseIntegrityOk();
+            boolean databaseOk = false;
+            if (!mEntered.get()) {
+                // The integrity check can retrigger the corruption handler if the database is,
+                // indeed, corrupted.  Use mEntered to detect recursion and to skip retrying the
+                // integrity check on recursion.
+                mEntered.set(true);
+                databaseOk = dbObj.isDatabaseIntegrityOk();
+            }
+            // At this point the database state has been detected and there is no further danger
+            // of recursion.  Setting mEntered to false allows this object to be reused, although
+            // it is not obvious how such reuse would work.
+            mEntered.set(false);
+
             // close the database
             try {
                 dbObj.close();
@@ -122,4 +130,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
index 95b0e32..e8d90f5 100644
--- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
@@ -914,6 +914,24 @@
         verifyLookasideStats(true);
     }
 
+    void verifyLookasideStats(boolean expectDisabled) {
+        boolean dbStatFound = false;
+        SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo();
+        for (SQLiteDebug.DbStats dbStat : info.dbStats) {
+            if (dbStat.dbName.endsWith(mDatabaseFile.getName()) && !dbStat.arePoolStats) {
+                dbStatFound = true;
+                Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside);
+                if (expectDisabled) {
+                    assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0);
+                } else {
+                    assertTrue("lookaside slots count should be greater than zero",
+                            dbStat.lookaside > 0);
+                }
+            }
+        }
+        assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound);
+    }
+
     @SmallTest
     public void testOpenParamsSetLookasideConfigValidation() {
         try {
@@ -930,24 +948,6 @@
         }
     }
 
-    void verifyLookasideStats(boolean expectDisabled) {
-        boolean dbStatFound = false;
-        SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo();
-        for (SQLiteDebug.DbStats dbStat : info.dbStats) {
-            if (dbStat.dbName.endsWith(mDatabaseFile.getName())) {
-                dbStatFound = true;
-                Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside);
-                if (expectDisabled) {
-                    assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0);
-                } else {
-                    assertTrue("lookaside slots count should be greater than zero",
-                            dbStat.lookaside > 0);
-                }
-            }
-        }
-        assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound);
-    }
-
     @LargeTest
     public void testDefaultDatabaseErrorHandler() {
         DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler();
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 94e23e7..87e6b18 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -427,12 +427,6 @@
       "group": "WM_DEBUG_BACK_PREVIEW",
       "at": "com\/android\/server\/wm\/BackNavigationController.java"
     },
-    "-1715268616": {
-      "message": "Last window, removing starting window %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STARTING_WINDOW",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
-    },
     "-1710206702": {
       "message": "Display id=%d is frozen while keyguard locked, return %d",
       "level": "VERBOSE",
@@ -463,6 +457,12 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
     },
+    "-1671601441": {
+      "message": "attachWindowContextToDisplayContent: calling from non-existing process pid=%d uid=%d",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-1670695197": {
       "message": "Attempted to add presentation window to a non-suitable display.  Aborting.",
       "level": "WARN",
@@ -1225,6 +1225,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "-961053385": {
+      "message": "attachWindowContextToDisplayArea: calling from non-existing process pid=%d uid=%d",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-957060823": {
       "message": "Moving to PAUSING: %s",
       "level": "VERBOSE",
@@ -4057,12 +4063,6 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "1671994402": {
-      "message": "Nulling last startingData",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STARTING_WINDOW",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
-    },
     "1674747211": {
       "message": "%s forcing orientation to %d for display id=%d",
       "level": "VERBOSE",
@@ -4243,12 +4243,6 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1853793312": {
-      "message": "Notify removed startingWindow %s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STARTING_WINDOW",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
-    },
     "1856783490": {
       "message": "resumeTopActivity: Restarting %s",
       "level": "DEBUG",
@@ -4279,6 +4273,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "1879463933": {
+      "message": "attachWindowContextToWindowToken: calling from non-existing process pid=%d uid=%d",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "1891501279": {
       "message": "cancelAnimation(): reason=%s",
       "level": "DEBUG",
diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index a5184f2..635e78e 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -276,9 +276,7 @@
     @CriticalNative
     private static native void nResetDisplayListCanvas(long canvas, long node,
             int width, int height);
-    @CriticalNative
     private static native int nGetMaximumTextureWidth();
-    @CriticalNative
     private static native int nGetMaximumTextureHeight();
     @CriticalNative
     private static native void nEnableZ(long renderer, boolean enableZ);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index d94e8e4..4d73c20 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -17,7 +17,9 @@
 package androidx.window.extensions.embedding;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
 
 import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -340,6 +342,20 @@
         wct.deleteTaskFragment(fragmentToken);
     }
 
+    void reorderTaskFragmentToFront(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_REORDER_TO_FRONT).build();
+        wct.addTaskFragmentOperation(fragmentToken, operation);
+    }
+
+    void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, boolean isolatedNav) {
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build();
+        wct.addTaskFragmentOperation(fragmentToken, operation);
+    }
+
     void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
         mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index a2f75e0..f95f3ff 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -250,6 +250,10 @@
             // Updates the Split
             final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
             final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+            mPresenter.setTaskFragmentIsolatedNavigation(wct,
+                    splitPinContainer.getSecondaryContainer().getTaskFragmentToken(),
+                    true /* isolatedNav */);
             mPresenter.updateSplitContainer(splitPinContainer, wct);
             transactionRecord.apply(false /* shouldApplyIndependently */);
             updateCallbackIfNecessary();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 4dafbd1..5de6acf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -17,7 +17,6 @@
 package androidx.window.extensions.embedding;
 
 import static android.content.pm.PackageManager.MATCH_ALL;
-import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
 
 import android.app.Activity;
 import android.app.ActivityThread;
@@ -40,7 +39,6 @@
 import android.view.WindowMetrics;
 import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentCreationParams;
-import android.window.TaskFragmentOperation;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.IntDef;
@@ -427,10 +425,8 @@
         final SplitPinContainer pinnedContainer =
                 container.getTaskContainer().getSplitPinContainer();
         if (pinnedContainer != null) {
-            final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
-                    OP_TYPE_REORDER_TO_FRONT).build();
-            wct.addTaskFragmentOperation(
-                    pinnedContainer.getSecondaryContainer().getTaskFragmentToken(), operation);
+            reorderTaskFragmentToFront(wct,
+                    pinnedContainer.getSecondaryContainer().getTaskFragmentToken());
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 208ae84..b062fbd 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -24,6 +24,11 @@
 }
 
 filegroup {
+    name: "WMShellFlickerTestsUtils-src",
+    srcs: ["src/com/android/wm/shell/flicker/utils/*.kt"],
+}
+
+filegroup {
     name: "WMShellFlickerTestsBase-src",
     srcs: ["src/com/android/wm/shell/flicker/*.kt"],
 }
@@ -53,6 +58,28 @@
     ],
 }
 
+java_library {
+    name: "wm-shell-flicker-utils",
+    platform_apis: true,
+    optimize: {
+        enabled: false,
+    },
+    srcs: [
+        ":WMShellFlickerTestsUtils-src",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "flickertestapplib",
+        "flickerlib",
+        "flickerlib-helpers",
+        "platform-test-annotations",
+        "wm-flicker-common-app-helpers",
+        "wm-flicker-common-assertions",
+        "launcher-helper-lib",
+        "launcher-aosp-tapl",
+    ],
+}
+
 java_defaults {
     name: "WMShellFlickerTestsDefault",
     manifest: "manifests/AndroidManifest.xml",
@@ -65,6 +92,7 @@
     test_suites: ["device-tests"],
     libs: ["android.test.runner"],
     static_libs: [
+        "wm-shell-flicker-utils",
         "androidx.test.ext.junit",
         "flickertestapplib",
         "flickerlib",
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index d2fe9fe..735fbfb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -21,6 +21,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.wm.shell.flicker.utils.ICommonAssertions
 
 /**
  * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR],
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 69c8ecd..77f14f1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -24,11 +24,10 @@
 import com.android.server.wm.flicker.helpers.LetterboxAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.wm.shell.flicker.BaseTest
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
@@ -37,9 +36,7 @@
     protected val context: Context = instrumentation.context
     protected val letterboxApp = LetterboxAppHelper(instrumentation)
 
-    @JvmField
-    @Rule
-    val letterboxRule: LetterboxRule = LetterboxRule()
+    @JvmField @Rule val letterboxRule: LetterboxRule = LetterboxRule()
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
index 5a1136f..744e8c2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
@@ -23,16 +23,14 @@
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
-/**
- * JUnit Rule to handle letterboxStyles and states
- */
+/** JUnit Rule to handle letterboxStyles and states */
 class LetterboxRule(
-        private val withLetterboxEducationEnabled: Boolean = false,
-        private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
-        private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+    private val withLetterboxEducationEnabled: Boolean = false,
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+    private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
 ) : TestRule {
 
-    private val execAdb: (String) -> String = {cmd -> cmdHelper.executeShellCommand(cmd)}
+    private val execAdb: (String) -> String = { cmd -> cmdHelper.executeShellCommand(cmd) }
     private lateinit var _letterboxStyle: MutableMap<String, String>
 
     val letterboxStyle: Map<String, String>
@@ -62,8 +60,7 @@
         var hasLetterboxEducationStateChanged = false
         if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) {
             hasLetterboxEducationStateChanged = true
-            execAdb("wm set-letterbox-style --isEducationEnabled " +
-                    withLetterboxEducationEnabled)
+            execAdb("wm set-letterbox-style --isEducationEnabled " + withLetterboxEducationEnabled)
         }
         return try {
             object : Statement() {
@@ -74,8 +71,8 @@
             }
         } finally {
             if (hasLetterboxEducationStateChanged) {
-                execAdb("wm set-letterbox-style --isEducationEnabled " +
-                        isLetterboxEducationEnabled
+                execAdb(
+                    "wm set-letterbox-style --isEducationEnabled " + isLetterboxEducationEnabled
                 )
             }
             resetLetterboxStyle()
@@ -100,9 +97,10 @@
         execAdb("wm reset-letterbox-style")
     }
 
-    private fun asInt(str: String?): Int? = try {
-        str?.toInt()
-    } catch (e: NumberFormatException) {
-        null
-    }
-}
\ No newline at end of file
+    private fun asInt(str: String?): Int? =
+        try {
+            str?.toInt()
+        } catch (e: NumberFormatException) {
+            null
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
index e6ca261..2fa1ec3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -54,9 +54,7 @@
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
-            setup {
-                letterboxTranslucentLauncherApp.launchViaIntent(wmHelper)
-            }
+            setup { letterboxTranslucentLauncherApp.launchViaIntent(wmHelper) }
             transitions {
                 waitAndGetLaunchTransparent()?.click() ?: error("Launch Transparent not found")
             }
@@ -66,9 +64,7 @@
             }
         }
 
-    /**
-     * Checks the transparent activity is launched on top of the opaque one
-     */
+    /** Checks the transparent activity is launched on top of the opaque one */
     @Postsubmit
     @Test
     fun translucentActivityIsLaunchedOnTopOfOpaqueActivity() {
@@ -79,18 +75,14 @@
         }
     }
 
-    /**
-     * Checks that the activity is letterboxed
-     */
+    /** Checks that the activity is letterboxed */
     @Postsubmit
     @Test
     fun translucentActivityIsLetterboxed() {
         flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) }
     }
 
-    /**
-     * Checks that the translucent activity inherits bounds from the opaque one.
-     */
+    /** Checks that the translucent activity inherits bounds from the opaque one. */
     @Postsubmit
     @Test
     fun translucentActivityInheritsBoundsFromOpaqueActivity() {
@@ -100,29 +92,26 @@
         }
     }
 
-    /**
-     * Checks that the translucent activity has rounded corners
-     */
+    /** Checks that the translucent activity has rounded corners */
     @Postsubmit
     @Test
     fun translucentActivityHasRoundedCorners() {
-        flicker.assertLayersEnd {
-            this.hasRoundedCorners(letterboxTranslucentApp)
-        }
+        flicker.assertLayersEnd { this.hasRoundedCorners(letterboxTranslucentApp) }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
-         * navigation modes.
+         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and navigation
+         * modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return LegacyFlickerTestFactory
-                .nonRotationTests(supportedRotations = listOf(Rotation.ROTATION_90))
+            return LegacyFlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(Rotation.ROTATION_90)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
index 47a7e65..b74aa1d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
@@ -49,8 +49,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) :
-    BaseAppCompat(flicker) {
+class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
index ea0392c..9792c85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
@@ -33,20 +33,20 @@
 
 abstract class TransparentBaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected val context: Context = instrumentation.context
-    protected val letterboxTranslucentLauncherApp = LetterboxAppHelper(
-        instrumentation,
-        launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
-        component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
-    )
-    protected val letterboxTranslucentApp = LetterboxAppHelper(
-        instrumentation,
-        launcherName = ActivityOptions.TransparentActivity.LABEL,
-        component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
-    )
+    protected val letterboxTranslucentLauncherApp =
+        LetterboxAppHelper(
+            instrumentation,
+            launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
+            component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
+        )
+    protected val letterboxTranslucentApp =
+        LetterboxAppHelper(
+            instrumentation,
+            launcherName = ActivityOptions.TransparentActivity.LABEL,
+            component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
+        )
 
-    @JvmField
-    @Rule
-    val letterboxRule: LetterboxRule = LetterboxRule()
+    @JvmField @Rule val letterboxRule: LetterboxRule = LetterboxRule()
 
     @Before
     fun before() {
@@ -54,10 +54,7 @@
     }
 
     protected fun FlickerTestData.waitAndGetLaunchTransparent(): UiObject2? =
-        device.wait(
-            Until.findObject(By.text("Launch Transparent")),
-            FIND_TIMEOUT
-        )
+        device.wait(Until.findObject(By.text("Launch Transparent")), FIND_TIMEOUT)
 
     protected fun FlickerTestData.goBack() = device.pressBack()
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index f165cb1..9cc9fb9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -42,8 +42,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FlakyTest(bugId = 217777115)
-class ChangeActiveActivityFromBubbleTest(flicker: LegacyFlickerTest) :
-    BaseBubbleScreen(flicker) {
+class ChangeActiveActivityFromBubbleTest(flicker: LegacyFlickerTest) : BaseBubbleScreen(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
index 8bd44c3..c335d3d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
@@ -28,7 +28,7 @@
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.SplitScreenUtils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
index 4f88184..421ad75 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
@@ -21,7 +21,7 @@
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
index 9a2fa09..a8fb63d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -22,7 +22,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
 import org.junit.Test
 import org.junit.runners.Parameterized
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
index afb4af6..992f1bc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
@@ -21,7 +21,7 @@
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
index 0432a84..d4cd6da 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
@@ -20,8 +20,8 @@
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.UiObject2
 import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.wait
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.wait
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
index 90406c5..4402e21 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
@@ -21,11 +21,11 @@
 import android.os.Bundle
 import android.service.notification.StatusBarNotification
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.NotificationListener.Companion.findNotification
-import com.android.wm.shell.flicker.NotificationListener.Companion.startNotificationListener
-import com.android.wm.shell.flicker.NotificationListener.Companion.stopNotificationListener
-import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToAppear
-import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToDisappear
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.findNotification
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.startNotificationListener
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.stopNotificationListener
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.waitForNotificationToAppear
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.waitForNotificationToDisappear
 import org.junit.After
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
index 6104b7b..47bff8d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
@@ -24,7 +24,7 @@
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import android.view.Surface.ROTATION_0
 import android.view.Surface.rotationToString
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
 import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assume.assumeTrue
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
index b0adbe1..4aee61a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
@@ -22,7 +22,7 @@
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
 
 /** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */
 private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
deleted file mode 100644
index e640dc4..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
+++ /dev/null
@@ -1,387 +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.wm.shell.flicker.service.splitscreen
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.common.traces.component.IComponentMatcher
-import android.tools.common.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.traces.parsers.WindowManagerStateHelper
-import android.tools.device.traces.parsers.toFlickerComponent
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
-
-object SplitScreenUtils {
-    private const val TIMEOUT_MS = 3_000L
-    private const val DRAG_DURATION_MS = 1_000L
-    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
-    private const val DIVIDER_BAR = "docked_divider_handle"
-    private const val OVERVIEW_SNAPSHOT = "snapshot"
-    private const val GESTURE_STEP_MS = 16L
-    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
-    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
-
-    private val notificationScrollerSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
-    private val notificationContentSelector: BySelector
-        get() = By.text("Flicker Test Notification")
-    private val dividerBarSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
-    private val overviewSnapshotSelector: BySelector
-        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
-
-    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Primary.LABEL,
-            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Secondary.LABEL,
-            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
-        NonResizeableAppHelper(instrumentation)
-
-    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
-        NotificationAppHelper(instrumentation)
-
-    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
-
-    fun waitForSplitComplete(
-        wmHelper: WindowManagerStateHelper,
-        primaryApp: IComponentMatcher,
-        secondaryApp: IComponentMatcher,
-    ) {
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceAppeared(primaryApp)
-            .withWindowSurfaceAppeared(secondaryApp)
-            .withSplitDividerVisible()
-            .waitForAndVerify()
-    }
-
-    fun enterSplit(
-        wmHelper: WindowManagerStateHelper,
-        tapl: LauncherInstrumentation,
-        device: UiDevice,
-        primaryApp: StandardAppHelper,
-        secondaryApp: StandardAppHelper
-    ) {
-        primaryApp.launchViaIntent(wmHelper)
-        secondaryApp.launchViaIntent(wmHelper)
-        tapl.goHome()
-        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-        splitFromOverview(tapl, device)
-        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
-    }
-
-    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
-        // Note: The initial split position in landscape is different between tablet and phone.
-        // In landscape, tablet will let the first app split to right side, and phone will
-        // split to left side.
-        if (tapl.isTablet) {
-            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
-            // contains more than 3 task views. We need to use uiautomator directly to find the
-            // second task to split.
-            tapl.workspace.switchToOverview().overviewActions.clickSplit()
-            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
-            if (snapshots == null || snapshots.size < 1) {
-                error("Fail to find a overview snapshot to split.")
-            }
-
-            // Find the second task in the upper right corner in split select mode by sorting
-            // 'left' in descending order and 'top' in ascending order.
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t2.getVisibleBounds().left - t1.getVisibleBounds().left
-            }
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t1.getVisibleBounds().top - t2.getVisibleBounds().top
-            }
-            snapshots[0].click()
-        } else {
-            tapl.workspace
-                .switchToOverview()
-                .currentTask
-                .tapMenu()
-                .tapSplitMenuItem()
-                .currentTask
-                .open()
-        }
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun enterSplitViaIntent(
-        wmHelper: WindowManagerStateHelper,
-        primaryApp: StandardAppHelper,
-        secondaryApp: StandardAppHelper
-    ) {
-        val stringExtras =
-            mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
-        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
-        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
-    }
-
-    fun dragFromNotificationToSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-
-        // Pull down the notifications
-        device.swipe(
-            displayBounds.centerX(),
-            5,
-            displayBounds.centerX(),
-            displayBounds.bottom,
-            50 /* steps */
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-
-        // Find the target notification
-        val notificationScroller =
-            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
-                ?: error("Unable to find view $notificationScrollerSelector")
-        var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
-        while (notificationContent == null) {
-            device.swipe(
-                displayBounds.centerX(),
-                displayBounds.centerY(),
-                displayBounds.centerX(),
-                displayBounds.centerY() - 150,
-                20 /* steps */
-            )
-            notificationContent = notificationScroller.findObject(notificationContentSelector)
-        }
-
-        // Drag to split
-        val dragStart = notificationContent.visibleCenter
-        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
-        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
-        val downTime = SystemClock.uptimeMillis()
-
-        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
-        // It needs a horizontal movement to trigger the drag
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragStart,
-            dragMiddle
-        )
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragMiddle,
-            dragEnd
-        )
-        // Wait for a while to start splitting
-        SystemClock.sleep(TIMEOUT_MS)
-        touch(
-            instrumentation,
-            MotionEvent.ACTION_UP,
-            downTime,
-            SystemClock.uptimeMillis(),
-            GESTURE_STEP_MS,
-            dragEnd
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun touch(
-        instrumentation: Instrumentation,
-        action: Int,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        point: Point
-    ) {
-        val motionEvent =
-            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
-        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
-        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
-        motionEvent.recycle()
-        SystemClock.sleep(duration)
-    }
-
-    fun touchMove(
-        instrumentation: Instrumentation,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        from: Point,
-        to: Point
-    ) {
-        val steps: Long = duration / GESTURE_STEP_MS
-        var currentTime = eventTime
-        var currentX = from.x.toFloat()
-        var currentY = from.y.toFloat()
-        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
-        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
-        for (i in 1..steps) {
-            val motionMove =
-                MotionEvent.obtain(
-                    downTime,
-                    currentTime,
-                    MotionEvent.ACTION_MOVE,
-                    currentX,
-                    currentY,
-                    0
-                )
-            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
-            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
-            motionMove.recycle()
-
-            currentTime += GESTURE_STEP_MS
-            if (i == steps - 1) {
-                currentX = to.x.toFloat()
-                currentY = to.y.toFloat()
-            } else {
-                currentX += stepX
-                currentY += stepY
-            }
-            SystemClock.sleep(GESTURE_STEP_MS)
-        }
-    }
-
-    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
-        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
-        val allApps = tapl.workspace.switchToAllApps()
-        allApps.freeze()
-        try {
-            allApps.getAppIcon(appName).dragToHotseat(0)
-        } finally {
-            allApps.unfreeze()
-        }
-    }
-
-    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
-
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
-            .waitForAndVerify()
-    }
-
-    fun dragDividerToDismissSplit(
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper,
-        dragToRight: Boolean,
-        dragToBottom: Boolean
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(
-            Point(
-                if (dragToRight) {
-                    displayBounds.width * 4 / 5
-                } else {
-                    displayBounds.width * 1 / 5
-                },
-                if (dragToBottom) {
-                    displayBounds.height * 4 / 5
-                } else {
-                    displayBounds.height * 1 / 5
-                }
-            )
-        )
-    }
-
-    fun doubleTapDividerToSwitch(device: UiDevice) {
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        val interval =
-            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
-        dividerBar.click()
-        SystemClock.sleep(interval.toLong())
-        dividerBar.click()
-    }
-
-    fun copyContentInSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        sourceApp: IComponentNameMatcher,
-        destinationApp: IComponentNameMatcher,
-    ) {
-        // Copy text from sourceApp
-        val textView =
-            device.wait(
-                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the TextView", textView)
-        textView.click(LONG_PRESS_TIME_MS)
-
-        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
-        assertNotNull("Unable to find the copy button", copyBtn)
-        copyBtn.click()
-
-        // Paste text to destinationApp
-        val editText =
-            device.wait(
-                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the EditText", editText)
-        editText.click(LONG_PRESS_TIME_MS)
-
-        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
-        assertNotNull("Unable to find the paste button", pasteBtn)
-        pasteBtn.click()
-
-        // Verify text
-        if (!textView.text.contentEquals(editText.text)) {
-            error("Fail to copy content in split")
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
index 5bfc889..e530f63 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
index d07daff..e9fc437 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
index d428dea..416692c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
index dc2a6ac..494a246 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
index 74f441d..a3aae85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
index 8d93c0d..50151f1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
index 36a458f..5d67dc7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
index ed08041..ae5bb68 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
index f164451..c2100f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
deleted file mode 100644
index 3831c65..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
+++ /dev/null
@@ -1,387 +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.wm.shell.flicker.service.splitscreen.scenarios
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.common.traces.component.IComponentMatcher
-import android.tools.common.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.traces.parsers.WindowManagerStateHelper
-import android.tools.device.traces.parsers.toFlickerComponent
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
-
-object SplitScreenUtils {
-    private const val TIMEOUT_MS = 3_000L
-    private const val DRAG_DURATION_MS = 1_000L
-    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
-    private const val DIVIDER_BAR = "docked_divider_handle"
-    private const val OVERVIEW_SNAPSHOT = "snapshot"
-    private const val GESTURE_STEP_MS = 16L
-    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
-    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
-
-    private val notificationScrollerSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
-    private val notificationContentSelector: BySelector
-        get() = By.text("Flicker Test Notification")
-    private val dividerBarSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
-    private val overviewSnapshotSelector: BySelector
-        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
-
-    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Primary.LABEL,
-            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Secondary.LABEL,
-            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
-        NonResizeableAppHelper(instrumentation)
-
-    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
-        NotificationAppHelper(instrumentation)
-
-    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
-
-    fun waitForSplitComplete(
-        wmHelper: WindowManagerStateHelper,
-        primaryApp: IComponentMatcher,
-        secondaryApp: IComponentMatcher,
-    ) {
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceAppeared(primaryApp)
-            .withWindowSurfaceAppeared(secondaryApp)
-            .withSplitDividerVisible()
-            .waitForAndVerify()
-    }
-
-    fun enterSplit(
-        wmHelper: WindowManagerStateHelper,
-        tapl: LauncherInstrumentation,
-        device: UiDevice,
-        primaryApp: StandardAppHelper,
-        secondaryApp: StandardAppHelper
-    ) {
-        primaryApp.launchViaIntent(wmHelper)
-        secondaryApp.launchViaIntent(wmHelper)
-        tapl.goHome()
-        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-        splitFromOverview(tapl, device)
-        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
-    }
-
-    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
-        // Note: The initial split position in landscape is different between tablet and phone.
-        // In landscape, tablet will let the first app split to right side, and phone will
-        // split to left side.
-        if (tapl.isTablet) {
-            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
-            // contains more than 3 task views. We need to use uiautomator directly to find the
-            // second task to split.
-            tapl.workspace.switchToOverview().overviewActions.clickSplit()
-            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
-            if (snapshots == null || snapshots.size < 1) {
-                error("Fail to find a overview snapshot to split.")
-            }
-
-            // Find the second task in the upper right corner in split select mode by sorting
-            // 'left' in descending order and 'top' in ascending order.
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t2.getVisibleBounds().left - t1.getVisibleBounds().left
-            }
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t1.getVisibleBounds().top - t2.getVisibleBounds().top
-            }
-            snapshots[0].click()
-        } else {
-            tapl.workspace
-                .switchToOverview()
-                .currentTask
-                .tapMenu()
-                .tapSplitMenuItem()
-                .currentTask
-                .open()
-        }
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun enterSplitViaIntent(
-        wmHelper: WindowManagerStateHelper,
-        primaryApp: StandardAppHelper,
-        secondaryApp: StandardAppHelper
-    ) {
-        val stringExtras =
-            mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
-        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
-        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
-    }
-
-    fun dragFromNotificationToSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-
-        // Pull down the notifications
-        device.swipe(
-            displayBounds.centerX(),
-            5,
-            displayBounds.centerX(),
-            displayBounds.bottom,
-            50 /* steps */
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-
-        // Find the target notification
-        val notificationScroller =
-            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
-                ?: error("Unable to find view $notificationScrollerSelector")
-        var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
-        while (notificationContent == null) {
-            device.swipe(
-                displayBounds.centerX(),
-                displayBounds.centerY(),
-                displayBounds.centerX(),
-                displayBounds.centerY() - 150,
-                20 /* steps */
-            )
-            notificationContent = notificationScroller.findObject(notificationContentSelector)
-        }
-
-        // Drag to split
-        val dragStart = notificationContent.visibleCenter
-        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
-        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
-        val downTime = SystemClock.uptimeMillis()
-
-        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
-        // It needs a horizontal movement to trigger the drag
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragStart,
-            dragMiddle
-        )
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragMiddle,
-            dragEnd
-        )
-        // Wait for a while to start splitting
-        SystemClock.sleep(TIMEOUT_MS)
-        touch(
-            instrumentation,
-            MotionEvent.ACTION_UP,
-            downTime,
-            SystemClock.uptimeMillis(),
-            GESTURE_STEP_MS,
-            dragEnd
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun touch(
-        instrumentation: Instrumentation,
-        action: Int,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        point: Point
-    ) {
-        val motionEvent =
-            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
-        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
-        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
-        motionEvent.recycle()
-        SystemClock.sleep(duration)
-    }
-
-    fun touchMove(
-        instrumentation: Instrumentation,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        from: Point,
-        to: Point
-    ) {
-        val steps: Long = duration / GESTURE_STEP_MS
-        var currentTime = eventTime
-        var currentX = from.x.toFloat()
-        var currentY = from.y.toFloat()
-        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
-        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
-        for (i in 1..steps) {
-            val motionMove =
-                MotionEvent.obtain(
-                    downTime,
-                    currentTime,
-                    MotionEvent.ACTION_MOVE,
-                    currentX,
-                    currentY,
-                    0
-                )
-            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
-            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
-            motionMove.recycle()
-
-            currentTime += GESTURE_STEP_MS
-            if (i == steps - 1) {
-                currentX = to.x.toFloat()
-                currentY = to.y.toFloat()
-            } else {
-                currentX += stepX
-                currentY += stepY
-            }
-            SystemClock.sleep(GESTURE_STEP_MS)
-        }
-    }
-
-    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
-        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
-        val allApps = tapl.workspace.switchToAllApps()
-        allApps.freeze()
-        try {
-            allApps.getAppIcon(appName).dragToHotseat(0)
-        } finally {
-            allApps.unfreeze()
-        }
-    }
-
-    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
-
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
-            .waitForAndVerify()
-    }
-
-    fun dragDividerToDismissSplit(
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper,
-        dragToRight: Boolean,
-        dragToBottom: Boolean
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(
-            Point(
-                if (dragToRight) {
-                    displayBounds.width * 4 / 5
-                } else {
-                    displayBounds.width * 1 / 5
-                },
-                if (dragToBottom) {
-                    displayBounds.height * 4 / 5
-                } else {
-                    displayBounds.height * 1 / 5
-                }
-            )
-        )
-    }
-
-    fun doubleTapDividerToSwitch(device: UiDevice) {
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        val interval =
-            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
-        dividerBar.click()
-        SystemClock.sleep(interval.toLong())
-        dividerBar.click()
-    }
-
-    fun copyContentInSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        sourceApp: IComponentNameMatcher,
-        destinationApp: IComponentNameMatcher,
-    ) {
-        // Copy text from sourceApp
-        val textView =
-            device.wait(
-                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the TextView", textView)
-        textView.click(LONG_PRESS_TIME_MS)
-
-        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
-        assertNotNull("Unable to find the copy button", copyBtn)
-        copyBtn.click()
-
-        // Paste text to destinationApp
-        val editText =
-            device.wait(
-                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the EditText", editText)
-        editText.click(LONG_PRESS_TIME_MS)
-
-        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
-        assertNotNull("Unable to find the paste button", pasteBtn)
-        pasteBtn.click()
-
-        // Verify text
-        if (!textView.text.contentEquals(editText.text)) {
-            error("Fail to copy content in split")
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index 805d987..70f3bed 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -26,6 +26,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
index 4229ebb..86f394d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
index f2d56b9..d7b611e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
index 9f9d4bb..3cc5df0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
index e2c6ca6..4a9c32f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
index df98d8f..383a6b3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 1d4c4d2..3702be9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -25,12 +25,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsKeepVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
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 0b8f109..8b90630 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
@@ -24,14 +24,14 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
-import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByDividerBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesInvisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 38d4b40..50f6a38 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -23,12 +23,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
-import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByGoHomeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesInvisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 0d967eb..cc3b783 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -23,12 +23,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsChanges
 import com.android.wm.shell.flicker.splitscreen.benchmark.DragDividerToResizeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsChanges
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -57,7 +57,7 @@
     @Test
     fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @Presubmit
+    @FlakyTest(bugId = 291678271)
     @Test
     fun primaryAppLayerVisibilityChanges() {
         flicker.assertLayers {
@@ -69,7 +69,7 @@
         }
     }
 
-    @Presubmit
+    @FlakyTest(bugId = 291678271)
     @Test
     fun secondaryAppLayerVisibilityChanges() {
         flicker.assertLayers {
@@ -87,7 +87,7 @@
     @Test
     fun secondaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(secondaryApp)
 
-    @FlakyTest(bugId = 245472831)
+    @FlakyTest(bugId = 291678271)
     @Test
     fun primaryAppBoundsChanges() {
         flicker.splitAppLayerBoundsChanges(
@@ -97,7 +97,7 @@
         )
     }
 
-    @Presubmit
+    @FlakyTest(bugId = 291678271)
     @Test
     fun secondaryAppBoundsChanges() =
         flicker.splitAppLayerBoundsChanges(
@@ -106,6 +106,12 @@
             portraitPosTop = true
         )
 
+    @FlakyTest(bugId = 291678271)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    }
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index 05c0480..f8d1e1f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -25,16 +25,16 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromAllAppsBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index 3a75fa6..ff5d935 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -25,15 +25,15 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromNotificationBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index 6d73f92..7c71077 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -24,14 +24,14 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromShortcutBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 15cae69..8371706 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -25,16 +25,16 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromTaskbarBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index 90399fc..0bfdbb4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -23,14 +23,14 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenFromOverviewBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
index 3064bd7..540c11f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -21,7 +21,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.wm.shell.flicker.BaseBenchmarkTest
-import com.android.wm.shell.flicker.SplitScreenUtils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 
 abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
     protected val context: Context = instrumentation.context
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 f236c2d..fac97c8 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
@@ -24,13 +24,13 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchAppByDoubleTapDividerBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index a406009..88bbc0e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -24,12 +24,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromAnotherAppBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 251bd10..e85dc24 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -24,12 +24,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromHomeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index 1dd45fe..f7a9ed0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -24,12 +24,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromRecentBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
index 8aaa98a..66f9b85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -23,15 +23,15 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBetweenSplitPairsBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsSnapToDivider
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 994d6cb..851391d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -24,12 +24,12 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.splitscreen.benchmark.UnlockKeyguardToSplitScreenBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
index d9d22de..e5c1e75 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
index 7e8d60b4..e4e1af9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
@@ -21,8 +21,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
index 770e032..b2dd02b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
@@ -21,8 +21,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
index 46570fd..0788591 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
@@ -21,8 +21,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
index 5c3d4ff..884e451 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
index 6b122c6..e5c40b6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
index 78f9bab..0451001 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
index 78907f0..9e0ca1b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
index 2c91e84..06b4fe7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
@@ -21,8 +21,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
index fa09c2e..007b751 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
@@ -25,8 +25,8 @@
 import android.tools.device.helpers.WindowUtils
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
index ff22006..10c8eeb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
index 5787b02..a6e750f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
index b2d5091..7e8d5fb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
index f234e46..56edad1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
@@ -21,8 +21,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
index 61c3679..065d4d6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
@@ -22,8 +22,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
similarity index 99%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
index 9cc03a5..e5c124c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
@@ -16,7 +16,7 @@
 
 @file:JvmName("CommonAssertions")
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.tools.common.Rotation
 import android.tools.common.datatypes.Region
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
index 3bc1e2a..3b66d6a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
@@ -16,7 +16,7 @@
 
 @file:JvmName("CommonConstants")
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.tools.common.traces.component.ComponentNameMatcher
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
similarity index 98%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
index 7b32901..7f58ced 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
similarity index 97%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
index 87b94ff..9b3a480 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.app.Instrumentation
 import android.content.Context
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
similarity index 98%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
index e0ef924..529c125 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.service.notification.NotificationListenerService
 import android.service.notification.StatusBarNotification
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
similarity index 99%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
index 8a3c2c9..3f8a1ae 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.app.Instrumentation
 import android.graphics.Point
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
index 556cb06..cf2df4e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
@@ -16,7 +16,7 @@
 
 @file:JvmName("WaitUtils")
 
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
 
 import android.os.SystemClock
 
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index dcbaaec..ad09067 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -91,12 +91,17 @@
   StringPoolRef entry_string_ref;
 };
 
-AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration)
-    : configuration_(configuration) {
+AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) {
+  configurations_.push_back(configuration);
+
   // Don't invalidate caches here as there's nothing cached yet.
   SetApkAssets(apk_assets, false);
 }
 
+AssetManager2::AssetManager2() {
+  configurations_.resize(1);
+}
+
 bool AssetManager2::SetApkAssets(ApkAssetsList apk_assets, bool invalidate_caches) {
   BuildDynamicRefTable(apk_assets);
   RebuildFilterList();
@@ -421,9 +426,16 @@
   return false;
 }
 
-void AssetManager2::SetConfiguration(const ResTable_config& configuration) {
-  const int diff = configuration_.diff(configuration);
-  configuration_ = configuration;
+void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations) {
+  int diff = 0;
+  if (configurations_.size() != configurations.size()) {
+    diff = -1;
+  } else {
+    for (int i = 0; i < configurations_.size(); i++) {
+      diff |= configurations_[i].diff(configurations[i]);
+    }
+  }
+  configurations_ = std::move(configurations);
 
   if (diff) {
     RebuildFilterList();
@@ -620,16 +632,6 @@
 
   auto op = StartOperation();
 
-  // Might use this if density_override != 0.
-  ResTable_config density_override_config;
-
-  // Select our configuration or generate a density override configuration.
-  const ResTable_config* desired_config = &configuration_;
-  if (density_override != 0 && density_override != configuration_.density) {
-    density_override_config = configuration_;
-    density_override_config.density = density_override;
-    desired_config = &density_override_config;
-  }
 
   // Retrieve the package group from the package id of the resource id.
   if (UNLIKELY(!is_valid_resid(resid))) {
@@ -648,119 +650,160 @@
   }
 
   const PackageGroup& package_group = package_groups_[package_idx];
-  auto result = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config,
-                                  stop_at_first_match, ignore_configuration);
-  if (UNLIKELY(!result.has_value())) {
-    return base::unexpected(result.error());
-  }
+  std::optional<FindEntryResult> final_result;
+  bool final_has_locale = false;
+  bool final_overlaid = false;
+  for (auto & config : configurations_) {
+    // Might use this if density_override != 0.
+    ResTable_config density_override_config;
 
-  bool overlaid = false;
-  if (!stop_at_first_match && !ignore_configuration) {
-    const auto& assets = GetApkAssets(result->cookie);
-    if (!assets) {
-      ALOGE("Found expired ApkAssets #%d for resource ID 0x%08x.", result->cookie, resid);
-      return base::unexpected(std::nullopt);
+    // Select our configuration or generate a density override configuration.
+    const ResTable_config* desired_config = &config;
+    if (density_override != 0 && density_override != config.density) {
+      density_override_config = config;
+      density_override_config.density = density_override;
+      desired_config = &density_override_config;
     }
-    if (!assets->IsLoader()) {
-      for (const auto& id_map : package_group.overlays_) {
-        auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
-        if (!overlay_entry) {
-          // No id map entry exists for this target resource.
-          continue;
-        }
-        if (overlay_entry.IsInlineValue()) {
-          // The target resource is overlaid by an inline value not represented by a resource.
-          ConfigDescription best_frro_config;
-          Res_value best_frro_value;
-          bool frro_found = false;
-          for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
-            if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
-                && config.match(*desired_config)) {
-              frro_found = true;
-              best_frro_config = config;
-              best_frro_value = value;
-            }
-          }
-          if (!frro_found) {
+
+    auto result = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config,
+                                    stop_at_first_match, ignore_configuration);
+    if (UNLIKELY(!result.has_value())) {
+      return base::unexpected(result.error());
+    }
+    bool overlaid = false;
+    if (!stop_at_first_match && !ignore_configuration) {
+      const auto& assets = GetApkAssets(result->cookie);
+      if (!assets) {
+        ALOGE("Found expired ApkAssets #%d for resource ID 0x%08x.", result->cookie, resid);
+        return base::unexpected(std::nullopt);
+      }
+      if (!assets->IsLoader()) {
+        for (const auto& id_map : package_group.overlays_) {
+          auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
+          if (!overlay_entry) {
+            // No id map entry exists for this target resource.
             continue;
           }
-          result->entry = best_frro_value;
+          if (overlay_entry.IsInlineValue()) {
+            // The target resource is overlaid by an inline value not represented by a resource.
+            ConfigDescription best_frro_config;
+            Res_value best_frro_value;
+            bool frro_found = false;
+            for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
+              if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
+                  && config.match(*desired_config)) {
+                frro_found = true;
+                best_frro_config = config;
+                best_frro_value = value;
+              }
+            }
+            if (!frro_found) {
+              continue;
+            }
+            result->entry = best_frro_value;
+            result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
+            result->cookie = id_map.cookie;
+
+            if (UNLIKELY(logging_enabled)) {
+              last_resolution_.steps.push_back(Resolution::Step{
+                  Resolution::Step::Type::OVERLAID_INLINE, result->cookie, String8()});
+              if (auto path = assets->GetPath()) {
+                const std::string overlay_path = path->data();
+                if (IsFabricatedOverlay(overlay_path)) {
+                  // FRRO don't have package name so we use the creating package here.
+                  String8 frro_name = String8("FRRO");
+                  // Get the first part of it since the expected one should be like
+                  // {overlayPackageName}-{overlayName}-{4 alphanumeric chars}.frro
+                  // under /data/resource-cache/.
+                  const std::string name = overlay_path.substr(overlay_path.rfind('/') + 1);
+                  const size_t end = name.find('-');
+                  if (frro_name.size() != overlay_path.size() && end != std::string::npos) {
+                    frro_name.append(base::StringPrintf(" created by %s",
+                                                        name.substr(0 /* pos */,
+                                                                    end).c_str()).c_str());
+                  }
+                  last_resolution_.best_package_name = frro_name;
+                } else {
+                  last_resolution_.best_package_name = result->package_name->c_str();
+                }
+              }
+              overlaid = true;
+            }
+            continue;
+          }
+
+          auto overlay_result = FindEntry(overlay_entry.GetResourceId(), density_override,
+                                          false /* stop_at_first_match */,
+                                          false /* ignore_configuration */);
+          if (UNLIKELY(IsIOError(overlay_result))) {
+            return base::unexpected(overlay_result.error());
+          }
+          if (!overlay_result.has_value()) {
+            continue;
+          }
+
+          if (!overlay_result->config.isBetterThan(result->config, desired_config)
+              && overlay_result->config.compare(result->config) != 0) {
+            // The configuration of the entry for the overlay must be equal to or better than the
+            // target configuration to be chosen as the better value.
+            continue;
+          }
+
+          result->cookie = overlay_result->cookie;
+          result->entry = overlay_result->entry;
+          result->config = overlay_result->config;
           result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
-          result->cookie = id_map.cookie;
 
           if (UNLIKELY(logging_enabled)) {
             last_resolution_.steps.push_back(
-                Resolution::Step{Resolution::Step::Type::OVERLAID_INLINE, result->cookie, String8()});
-            if (auto path = assets->GetPath()) {
-              const std::string overlay_path = path->data();
-              if (IsFabricatedOverlay(overlay_path)) {
-                // FRRO don't have package name so we use the creating package here.
-                String8 frro_name = String8("FRRO");
-                // Get the first part of it since the expected one should be like
-                // {overlayPackageName}-{overlayName}-{4 alphanumeric chars}.frro
-                // under /data/resource-cache/.
-                const std::string name = overlay_path.substr(overlay_path.rfind('/') + 1);
-                const size_t end = name.find('-');
-                if (frro_name.size() != overlay_path.size() && end != std::string::npos) {
-                  frro_name.append(base::StringPrintf(" created by %s",
-                                                      name.substr(0 /* pos */,
-                                                                  end).c_str()).c_str());
-                }
-                last_resolution_.best_package_name = frro_name;
-              } else {
-                last_resolution_.best_package_name = result->package_name->c_str();
-              }
-            }
+                Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->cookie,
+                                 overlay_result->config.toString()});
+            last_resolution_.best_package_name =
+                overlay_result->package_name->c_str();
             overlaid = true;
           }
-          continue;
-        }
-
-        auto overlay_result = FindEntry(overlay_entry.GetResourceId(), density_override,
-                                        false /* stop_at_first_match */,
-                                        false /* ignore_configuration */);
-        if (UNLIKELY(IsIOError(overlay_result))) {
-          return base::unexpected(overlay_result.error());
-        }
-        if (!overlay_result.has_value()) {
-          continue;
-        }
-
-        if (!overlay_result->config.isBetterThan(result->config, desired_config)
-            && overlay_result->config.compare(result->config) != 0) {
-          // The configuration of the entry for the overlay must be equal to or better than the target
-          // configuration to be chosen as the better value.
-          continue;
-        }
-
-        result->cookie = overlay_result->cookie;
-        result->entry = overlay_result->entry;
-        result->config = overlay_result->config;
-        result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
-
-        if (UNLIKELY(logging_enabled)) {
-          last_resolution_.steps.push_back(
-              Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->cookie,
-                               overlay_result->config.toString()});
-          last_resolution_.best_package_name =
-              overlay_result->package_name->c_str();
-          overlaid = true;
         }
       }
     }
+
+    bool has_locale = false;
+    if (result->config.locale == 0) {
+      if (default_locale_ != 0) {
+        ResTable_config conf;
+        conf.locale = default_locale_;
+        // Since we know conf has a locale and only a locale, match will tell us if that locale
+        // matches
+        has_locale = conf.match(config);
+      }
+    } else {
+      has_locale = true;
+    }
+
+    // if we don't have a result yet
+    if (!final_result ||
+        // or this config is better before the locale than the existing result
+        result->config.isBetterThanBeforeLocale(final_result->config, desired_config) ||
+        // or the existing config isn't better before locale and this one specifies a locale
+        // whereas the existing one doesn't
+        (!final_result->config.isBetterThanBeforeLocale(result->config, desired_config)
+            && has_locale && !final_has_locale)) {
+      final_result = result.value();
+      final_overlaid = overlaid;
+      final_has_locale = has_locale;
+    }
   }
 
   if (UNLIKELY(logging_enabled)) {
-    last_resolution_.cookie = result->cookie;
-    last_resolution_.type_string_ref = result->type_string_ref;
-    last_resolution_.entry_string_ref = result->entry_string_ref;
-    last_resolution_.best_config_name = result->config.toString();
-    if (!overlaid) {
-      last_resolution_.best_package_name = result->package_name->c_str();
+    last_resolution_.cookie = final_result->cookie;
+    last_resolution_.type_string_ref = final_result->type_string_ref;
+    last_resolution_.entry_string_ref = final_result->entry_string_ref;
+    last_resolution_.best_config_name = final_result->config.toString();
+    if (!final_overlaid) {
+      last_resolution_.best_package_name = final_result->package_name->c_str();
     }
   }
 
-  return result;
+  return *final_result;
 }
 
 base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal(
@@ -778,8 +821,10 @@
   // If `desired_config` is not the same as the set configuration or the caller will accept a value
   // from any configuration, then we cannot use our filtered list of types since it only it contains
   // types matched to the set configuration.
-  const bool use_filtered = !ignore_configuration && &desired_config == &configuration_;
-
+  const bool use_filtered = !ignore_configuration && std::find_if(
+      configurations_.begin(), configurations_.end(),
+      [&desired_config](auto& value) { return &desired_config == &value; })
+      != configurations_.end();
   const size_t package_count = package_group.packages_.size();
   for (size_t pi = 0; pi < package_count; pi++) {
     const ConfiguredPackage& loaded_package_impl = package_group.packages_[pi];
@@ -934,10 +979,22 @@
   }
 
   std::stringstream log_stream;
-  log_stream << base::StringPrintf("Resolution for 0x%08x %s\n"
-                                   "\tFor config - %s", resid, resource_name_string.c_str(),
-                                   configuration_.toString().c_str());
-
+  if (configurations_.size() == 1) {
+    log_stream << base::StringPrintf("Resolution for 0x%08x %s\n"
+                                     "\tFor config - %s", resid, resource_name_string.c_str(),
+                                     configurations_[0].toString().c_str());
+  } else {
+    ResTable_config conf = configurations_[0];
+    conf.clearLocale();
+    log_stream << base::StringPrintf("Resolution for 0x%08x %s\n\tFor config - %s and locales",
+                                     resid, resource_name_string.c_str(), conf.toString().c_str());
+    char str[40];
+    str[0] = '\0';
+    for(auto iter = configurations_.begin(); iter < configurations_.end(); iter++) {
+      iter->getBcp47Locale(str);
+      log_stream << base::StringPrintf(" %s%s", str, iter < configurations_.end() ? "," : "");
+    }
+  }
   for (const Resolution::Step& step : last_resolution_.steps) {
     constexpr static std::array kStepStrings = {
         "Found initial",
@@ -1427,11 +1484,14 @@
       package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) {
         FilteredConfigGroup* group = nullptr;
         for (const auto& type_entry : type_spec.type_entries) {
-          if (type_entry.config.match(configuration_)) {
-            if (!group) {
-              group = &package.filtered_configs_.editItemAt(type_id - 1);
+          for (auto & config : configurations_) {
+            if (type_entry.config.match(config)) {
+              if (!group) {
+                group = &package.filtered_configs_.editItemAt(type_id - 1);
+              }
+              group->type_entries.push_back(&type_entry);
+              break;
             }
-            group->type_entries.push_back(&type_entry);
           }
         }
       });
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 5a63612..06d19e0 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -2568,6 +2568,22 @@
     return false;
 }
 
+bool ResTable_config::isBetterThanBeforeLocale(const ResTable_config& o,
+        const ResTable_config* requested) const {
+    if (requested) {
+        if (imsi || o.imsi) {
+            if ((mcc != o.mcc) && requested->mcc) {
+                return (mcc);
+            }
+
+            if ((mnc != o.mnc) && requested->mnc) {
+                return (mnc);
+            }
+        }
+    }
+    return false;
+}
+
 bool ResTable_config::isBetterThan(const ResTable_config& o,
         const ResTable_config* requested) const {
     if (requested) {
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index f611d0d..d9ff35b 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -100,7 +100,7 @@
   using ApkAssetsWPtr = wp<const ApkAssets>;
   using ApkAssetsList = std::span<const ApkAssetsPtr>;
 
-  AssetManager2() = default;
+  AssetManager2();
   explicit AssetManager2(AssetManager2&& other) = default;
   AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration);
 
@@ -156,10 +156,14 @@
 
   // Sets/resets the configuration for this AssetManager. This will cause all
   // caches that are related to the configuration change to be invalidated.
-  void SetConfiguration(const ResTable_config& configuration);
+  void SetConfigurations(std::vector<ResTable_config> configurations);
 
-  inline const ResTable_config& GetConfiguration() const {
-    return configuration_;
+  inline const std::vector<ResTable_config>& GetConfigurations() const {
+    return configurations_;
+  }
+
+  inline void SetDefaultLocale(uint32_t default_locale) {
+    default_locale_ = default_locale;
   }
 
   // Returns all configurations for which there are resources defined, or an I/O error if reading
@@ -465,9 +469,11 @@
   // without taking too much memory.
   std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_;
 
-  // The current configuration set for this AssetManager. When this changes, cached resources
+  uint32_t default_locale_;
+
+  // The current configurations set for this AssetManager. When this changes, cached resources
   // may need to be purged.
-  ResTable_config configuration_ = {};
+  std::vector<ResTable_config> configurations_;
 
   // Cached set of bags. These are cached because they can inherit keys from parent bags,
   // which involves some calculation.
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 4eb1d7a..52666ab 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1375,6 +1375,8 @@
     // match the requested configuration at all.
     bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const;
 
+    bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const;
+
     String8 toString() const;
 };
 
diff --git a/libs/androidfw/tests/AssetManager2_bench.cpp b/libs/androidfw/tests/AssetManager2_bench.cpp
index 6fae72a..2caa98c 100644
--- a/libs/androidfw/tests/AssetManager2_bench.cpp
+++ b/libs/androidfw/tests/AssetManager2_bench.cpp
@@ -228,10 +228,12 @@
 
   ResTable_config config;
   memset(&config, 0, sizeof(config));
+  std::vector<ResTable_config> configs;
+  configs.push_back(config);
 
   while (state.KeepRunning()) {
-    config.sdkVersion = ~config.sdkVersion;
-    assets.SetConfiguration(config);
+    configs[0].sdkVersion = ~configs[0].sdkVersion;
+    assets.SetConfigurations(configs);
   }
 }
 BENCHMARK(BM_AssetManagerSetConfigurationFramework);
diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp
index df3fa02..c62f095 100644
--- a/libs/androidfw/tests/AssetManager2_test.cpp
+++ b/libs/androidfw/tests/AssetManager2_test.cpp
@@ -113,7 +113,7 @@
   desired_config.language[1] = 'e';
 
   AssetManager2 assetmanager;
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_});
 
   auto value = assetmanager.GetResource(basic::R::string::test1);
@@ -137,7 +137,7 @@
   desired_config.language[1] = 'e';
 
   AssetManager2 assetmanager;
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_});
 
   auto value = assetmanager.GetResource(basic::R::string::test1);
@@ -466,10 +466,10 @@
 TEST_F(AssetManager2Test, DensityOverride) {
   AssetManager2 assetmanager;
   assetmanager.SetApkAssets({basic_assets_, basic_xhdpi_assets_, basic_xxhdpi_assets_});
-  assetmanager.SetConfiguration({
+  assetmanager.SetConfigurations({{
     .density = ResTable_config::DENSITY_XHIGH,
     .sdkVersion = 21,
-  });
+  }});
 
   auto value = assetmanager.GetResource(basic::R::string::density, false /*may_be_bag*/);
   ASSERT_TRUE(value.has_value());
@@ -721,7 +721,7 @@
   ResTable_config desired_config;
 
   AssetManager2 assetmanager;
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_});
   assetmanager.SetResourceResolutionLoggingEnabled(false);
 
@@ -736,7 +736,7 @@
   ResTable_config desired_config;
 
   AssetManager2 assetmanager;
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_});
 
   auto result = assetmanager.GetLastResourceResolution();
@@ -751,7 +751,7 @@
 
   AssetManager2 assetmanager;
   assetmanager.SetResourceResolutionLoggingEnabled(true);
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_});
 
   auto value = assetmanager.GetResource(basic::R::string::test1);
@@ -774,7 +774,7 @@
 
   AssetManager2 assetmanager;
   assetmanager.SetResourceResolutionLoggingEnabled(true);
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_});
 
   auto value = assetmanager.GetResource(basic::R::string::test1);
@@ -796,7 +796,7 @@
 
   AssetManager2 assetmanager;
   assetmanager.SetResourceResolutionLoggingEnabled(true);
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({basic_assets_});
 
   auto value = assetmanager.GetResource(basic::R::string::test1);
@@ -817,7 +817,7 @@
 
   AssetManager2 assetmanager;
   assetmanager.SetResourceResolutionLoggingEnabled(true);
-  assetmanager.SetConfiguration(desired_config);
+  assetmanager.SetConfigurations({desired_config});
   assetmanager.SetApkAssets({overlayable_assets_});
 
   const auto map = assetmanager.GetOverlayableMapForPackage(0x7f);
diff --git a/libs/androidfw/tests/BenchmarkHelpers.cpp b/libs/androidfw/tests/BenchmarkHelpers.cpp
index b97dd96..8b883f4 100644
--- a/libs/androidfw/tests/BenchmarkHelpers.cpp
+++ b/libs/androidfw/tests/BenchmarkHelpers.cpp
@@ -66,7 +66,7 @@
   AssetManager2 assetmanager;
   assetmanager.SetApkAssets(apk_assets);
   if (config != nullptr) {
-    assetmanager.SetConfiguration(*config);
+    assetmanager.SetConfigurations({*config});
   }
 
   while (state.KeepRunning()) {
diff --git a/libs/androidfw/tests/Theme_test.cpp b/libs/androidfw/tests/Theme_test.cpp
index e08a6a7..181d141 100644
--- a/libs/androidfw/tests/Theme_test.cpp
+++ b/libs/androidfw/tests/Theme_test.cpp
@@ -260,7 +260,7 @@
   ResTable_config night{};
   night.uiMode = ResTable_config::UI_MODE_NIGHT_YES;
   night.version = 8u;
-  am_night.SetConfiguration(night);
+  am_night.SetConfigurations({night});
 
   auto theme = am.NewTheme();
   {
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index b785989..ced0224 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -175,7 +175,7 @@
                                   const Paint& paint, const SkPath& path, size_t start,
                                   size_t end) override;
 
-    void onFilterPaint(Paint& paint);
+    virtual void onFilterPaint(Paint& paint);
 
     Paint filterPaint(const Paint& src) {
         Paint dst(src);
diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
index f060bb3..426644e 100644
--- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
+++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
@@ -84,7 +84,7 @@
     canvas->resetRecording(width, height, renderNode);
 }
 
-static jint android_view_DisplayListCanvas_getMaxTextureSize(CRITICAL_JNI_PARAMS) {
+static jint android_view_DisplayListCanvas_getMaxTextureSize(JNIEnv*, jobject) {
 #ifdef __ANDROID__ // Layoutlib does not support RenderProxy (RenderThread)
     return android::uirenderer::renderthread::RenderProxy::maxTextureSize();
 #else
@@ -175,14 +175,14 @@
 const char* const kClassPathName = "android/graphics/RecordingCanvas";
 
 static JNINativeMethod gMethods[] = {
+        {"nGetMaximumTextureWidth", "()I", (void*)android_view_DisplayListCanvas_getMaxTextureSize},
+        {"nGetMaximumTextureHeight", "()I",
+         (void*)android_view_DisplayListCanvas_getMaxTextureSize},
         // ------------ @CriticalNative --------------
         {"nCreateDisplayListCanvas", "(JII)J",
          (void*)android_view_DisplayListCanvas_createDisplayListCanvas},
         {"nResetDisplayListCanvas", "(JJII)V",
          (void*)android_view_DisplayListCanvas_resetDisplayListCanvas},
-        {"nGetMaximumTextureWidth", "()I", (void*)android_view_DisplayListCanvas_getMaxTextureSize},
-        {"nGetMaximumTextureHeight", "()I",
-         (void*)android_view_DisplayListCanvas_getMaxTextureSize},
         {"nEnableZ", "(JZ)V", (void*)android_view_DisplayListCanvas_enableZ},
         {"nFinishRecording", "(JJ)V", (void*)android_view_DisplayListCanvas_finishRecording},
         {"nDrawRenderNode", "(JJ)V", (void*)android_view_DisplayListCanvas_drawRenderNode},
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 58c14c1..e917f9a 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -236,6 +236,17 @@
     }
 }
 
+void SkiaRecordingCanvas::onFilterPaint(android::Paint& paint) {
+    INHERITED::onFilterPaint(paint);
+    SkShader* shader = paint.getShader();
+    // TODO(b/264559422): This only works for very specifically a BitmapShader.
+    //  It's better than nothing, though
+    SkImage* image = shader ? shader->isAImage(nullptr, nullptr) : nullptr;
+    if (image) {
+        mDisplayList->mMutableImages.push_back(image);
+    }
+}
+
 void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) {
     auto payload = DrawImagePayload(bitmap);
 
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index a8e4580..3bd091d 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -105,6 +105,8 @@
 
     void handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload);
 
+    void onFilterPaint(Paint& paint) override;
+
     using INHERITED = SkiaCanvas;
 };
 
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 224c878..f949ddd 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -16,7 +16,13 @@
 
 #include "RenderProxy.h"
 
+#include <SkBitmap.h>
+#include <SkImage.h>
+#include <SkPicture.h>
 #include <gui/TraceUtils.h>
+#include <pthread.h>
+#include <ui/GraphicBufferAllocator.h>
+
 #include "DeferredLayerUpdater.h"
 #include "DisplayList.h"
 #include "Properties.h"
@@ -29,12 +35,6 @@
 #include "utils/Macros.h"
 #include "utils/TimeUtils.h"
 
-#include <SkBitmap.h>
-#include <SkImage.h>
-#include <SkPicture.h>
-
-#include <pthread.h>
-
 namespace android {
 namespace uirenderer {
 namespace renderthread {
@@ -323,6 +323,9 @@
             }
         });
     }
+    std::string grallocInfo;
+    GraphicBufferAllocator::getInstance().dump(grallocInfo);
+    dprintf(fd, "%s\n", grallocInfo.c_str());
 }
 
 void RenderProxy::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {
diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
index f6be7b2..064d42e 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -129,6 +129,33 @@
     EXPECT_EQ(counts.destroyed, 1);
 }
 
+TEST(SkiaDisplayList, recordMutableBitmap) {
+    SkiaRecordingCanvas canvas{nullptr, 100, 100};
+    auto bitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make(
+            10, 20, SkColorType::kN32_SkColorType, SkAlphaType::kPremul_SkAlphaType));
+    EXPECT_FALSE(bitmap->isImmutable());
+    canvas.drawBitmap(*bitmap, 0, 0, nullptr);
+    auto displayList = canvas.finishRecording();
+    ASSERT_EQ(1, displayList->mMutableImages.size());
+    EXPECT_EQ(10, displayList->mMutableImages[0]->width());
+    EXPECT_EQ(20, displayList->mMutableImages[0]->height());
+}
+
+TEST(SkiaDisplayList, recordMutableBitmapInShader) {
+    SkiaRecordingCanvas canvas{nullptr, 100, 100};
+    auto bitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make(
+            10, 20, SkColorType::kN32_SkColorType, SkAlphaType::kPremul_SkAlphaType));
+    EXPECT_FALSE(bitmap->isImmutable());
+    SkSamplingOptions sampling(SkFilterMode::kLinear, SkMipmapMode::kNone);
+    Paint paint;
+    paint.setShader(bitmap->makeImage()->makeShader(sampling));
+    canvas.drawPaint(paint);
+    auto displayList = canvas.finishRecording();
+    ASSERT_EQ(1, displayList->mMutableImages.size());
+    EXPECT_EQ(10, displayList->mMutableImages[0]->width());
+    EXPECT_EQ(20, displayList->mMutableImages[0]->height());
+}
+
 class ContextFactory : public IContextFactory {
 public:
     virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override {
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index fc33cef..8f1b9fe 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -25,6 +25,7 @@
 import android.system.OsConstants;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.midi.MidiDispatcher;
 
 import dalvik.system.CloseGuard;
@@ -34,6 +35,7 @@
 import java.io.Closeable;
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -83,6 +85,14 @@
     private AtomicInteger mTotalInputBytes = new AtomicInteger();
     private AtomicInteger mTotalOutputBytes = new AtomicInteger();
 
+    private static final int UNUSED_UID = -1;
+
+    private final Object mUmpUidLock = new Object();
+    @GuardedBy("mUmpUidLock")
+    private final int[] mUmpInputPortUids;
+    @GuardedBy("mUmpUidLock")
+    private final int[] mUmpOutputPortUids;
+
     public interface Callback {
         /**
          * Called to notify when an our device status has changed
@@ -137,6 +147,9 @@
                 int portNumber = mOutputPort.getPortNumber();
                 mInputPortOutputPorts[portNumber] = null;
                 mInputPortOpen[portNumber] = false;
+                synchronized (mUmpUidLock) {
+                    mUmpInputPortUids[portNumber] = UNUSED_UID;
+                }
                 mTotalOutputBytes.addAndGet(mOutputPort.pullTotalBytesCount());
                 updateTotalBytes();
                 updateDeviceStatus();
@@ -162,6 +175,9 @@
                 dispatcher.getSender().disconnect(mInputPort);
                 int openCount = dispatcher.getReceiverCount();
                 mOutputPortOpenCount[portNumber] = openCount;
+                synchronized (mUmpUidLock) {
+                    mUmpOutputPortUids[portNumber] = UNUSED_UID;
+                }
                 mTotalInputBytes.addAndGet(mInputPort.pullTotalBytesCount());
                 updateTotalBytes();
                 updateDeviceStatus();
@@ -210,6 +226,25 @@
                     return null;
                 }
 
+                if (isUmpDevice()) {
+                    if (portNumber >= mOutputPortCount) {
+                        Log.e(TAG, "out portNumber out of range in openInputPort: " + portNumber);
+                        return null;
+                    }
+                    synchronized (mUmpUidLock) {
+                        if (mUmpInputPortUids[portNumber] != UNUSED_UID) {
+                            Log.e(TAG, "input port already open in openInputPort: " + portNumber);
+                            return null;
+                        }
+                        if ((mUmpOutputPortUids[portNumber] != UNUSED_UID)
+                                && (Binder.getCallingUid() != mUmpOutputPortUids[portNumber])) {
+                            Log.e(TAG, "different uid for output in openInputPort: " + portNumber);
+                            return null;
+                        }
+                        mUmpInputPortUids[portNumber] = Binder.getCallingUid();
+                    }
+                }
+
                 try {
                     FileDescriptor[] pair = createSeqPacketSocketPair();
                     MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber);
@@ -242,6 +277,25 @@
                 return null;
             }
 
+            if (isUmpDevice()) {
+                if (portNumber >= mInputPortCount) {
+                    Log.e(TAG, "in portNumber out of range in openOutputPort: " + portNumber);
+                    return null;
+                }
+                synchronized (mUmpUidLock) {
+                    if (mUmpOutputPortUids[portNumber] != UNUSED_UID) {
+                        Log.e(TAG, "output port already open in openOutputPort: " + portNumber);
+                        return null;
+                    }
+                    if ((mUmpInputPortUids[portNumber] != UNUSED_UID)
+                            && (Binder.getCallingUid() != mUmpInputPortUids[portNumber])) {
+                        Log.e(TAG, "different uid for input in openOutputPort: " + portNumber);
+                        return null;
+                    }
+                    mUmpOutputPortUids[portNumber] = Binder.getCallingUid();
+                }
+            }
+
             try {
                 FileDescriptor[] pair = createSeqPacketSocketPair();
                 MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
@@ -358,6 +412,13 @@
         mInputPortOpen = new boolean[mInputPortCount];
         mOutputPortOpenCount = new int[numOutputPorts];
 
+        synchronized (mUmpUidLock) {
+            mUmpInputPortUids = new int[mInputPortCount];
+            mUmpOutputPortUids = new int[mOutputPortCount];
+            Arrays.fill(mUmpInputPortUids, UNUSED_UID);
+            Arrays.fill(mUmpOutputPortUids, UNUSED_UID);
+        }
+
         mGuard.open("close");
     }
 
@@ -467,4 +528,8 @@
             Log.e(TAG, "RemoteException in updateTotalBytes");
         }
     }
+
+    private boolean isUmpDevice() {
+        return mDeviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN;
+    }
 }
diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java
index 388d95b..96540a2 100644
--- a/media/java/android/media/midi/MidiDeviceService.java
+++ b/media/java/android/media/midi/MidiDeviceService.java
@@ -34,15 +34,15 @@
  * <p>To extend this class, you must declare the service in your manifest file with
  * an intent filter with the {@link #SERVICE_INTERFACE} action
  * and meta-data to describe the virtual device.
- For example:</p>
+ * For example:</p>
  * <pre>
  * &lt;service android:name=".VirtualDeviceService"
- *          android:label="&#64;string/service_name">
+ *         android:label="&#64;string/service_name">
  *     &lt;intent-filter>
- *         &lt;action android:name="android.media.midi.MidiDeviceService" />
+ *             &lt;action android:name="android.media.midi.MidiDeviceService" />
  *     &lt;/intent-filter>
- *           &lt;meta-data android:name="android.media.midi.MidiDeviceService"
-                android:resource="@xml/device_info" />
+ *     &lt;meta-data android:name="android.media.midi.MidiDeviceService"
+ *             android:resource="@xml/device_info" />
  * &lt;/service></pre>
  */
 abstract public class MidiDeviceService extends Service {
@@ -114,8 +114,8 @@
     }
 
     /**
-     * returns the {@link MidiDeviceInfo} instance for this service
-     * @return our MidiDeviceInfo
+     * Returns the {@link MidiDeviceInfo} instance for this service
+     * @return the MidiDeviceInfo of the virtual MIDI device
      */
     public final MidiDeviceInfo getDeviceInfo() {
         return mDeviceInfo;
@@ -123,13 +123,14 @@
 
     /**
      * Called to notify when an our {@link MidiDeviceStatus} has changed
-     * @param status the number of the port that was opened
+     * @param status the current status of the MIDI device
      */
     public void onDeviceStatusChanged(MidiDeviceStatus status) {
     }
 
     /**
-     * Called to notify when our device has been closed by all its clients
+     * Called to notify when the virtual MIDI device running in this service has been closed by
+     * all its clients
      */
     public void onClose() {
     }
diff --git a/media/java/android/media/midi/MidiUmpDeviceService.java b/media/java/android/media/midi/MidiUmpDeviceService.java
new file mode 100644
index 0000000..0c6096e
--- /dev/null
+++ b/media/java/android/media/midi/MidiUmpDeviceService.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.midi;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A service that implements a virtual MIDI device for Universal MIDI Packets (UMP).
+ * Subclasses must implement the {@link #onGetInputPortReceivers} method to provide a
+ * list of {@link MidiReceiver}s to receive data sent to the device's input ports.
+ * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list
+ * of {@link MidiReceiver}s for sending data out the output ports.
+ *
+ * Unlike traditional MIDI byte streams, only complete UMPs should be sent.
+ * Unlike with {@link #MidiDeviceService}, the number of input and output ports must be equal.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_INTERFACE} action
+ * and meta-data to describe the virtual device.
+ * For example:</p>
+ * <pre>
+ * &lt;service android:name=".VirtualDeviceService"
+ *         android:label="&#64;string/service_name">
+ *     &lt;intent-filter>
+ *             &lt;action android:name="android.media.midi.MidiUmpDeviceService" />
+ *     &lt;/intent-filter>
+ *     &lt;property android:name="android.media.midi.MidiUmpDeviceService"
+ *             android:resource="@xml/device_info" />
+ * &lt;/service></pre>
+ */
+public abstract class MidiUmpDeviceService extends Service {
+    private static final String TAG = "MidiUmpDeviceService";
+
+    public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService";
+
+    private IMidiManager mMidiManager;
+    private MidiDeviceServer mServer;
+    private MidiDeviceInfo mDeviceInfo;
+
+    private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
+        @Override
+        public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
+            MidiUmpDeviceService.this.onDeviceStatusChanged(status);
+        }
+
+        @Override
+        public void onClose() {
+            MidiUmpDeviceService.this.onClose();
+        }
+    };
+
+    @Override
+    public void onCreate() {
+        mMidiManager = IMidiManager.Stub.asInterface(
+                    ServiceManager.getService(Context.MIDI_SERVICE));
+        MidiDeviceServer server;
+        try {
+            MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(),
+                    this.getClass().getName());
+            if (deviceInfo == null) {
+                Log.e(TAG, "Could not find MidiDeviceInfo for MidiUmpDeviceService " + this);
+                return;
+            }
+            mDeviceInfo = deviceInfo;
+
+            List<MidiReceiver> inputPortReceivers = onGetInputPortReceivers();
+            if (inputPortReceivers == null) {
+                Log.e(TAG, "Could not get input port receivers for MidiUmpDeviceService " + this);
+                return;
+            }
+            MidiReceiver[] inputPortReceiversArr = new MidiReceiver[inputPortReceivers.size()];
+            inputPortReceivers.toArray(inputPortReceiversArr);
+            server = new MidiDeviceServer(mMidiManager, inputPortReceiversArr, deviceInfo,
+                    mCallback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo");
+            server = null;
+        }
+        mServer = server;
+    }
+
+    /**
+     * Returns a list of {@link MidiReceiver} for the device's input ports.
+     * Subclasses must override this to provide the receivers which will receive
+     * data sent to the device's input ports.
+     * The number of input and output ports must be equal and non-zero.
+     * @return list of MidiReceivers
+     */
+    public abstract @NonNull List<MidiReceiver> onGetInputPortReceivers();
+
+    /**
+     * Returns a list of {@link MidiReceiver} for the device's output ports.
+     * These can be used to send data out the device's output ports.
+     * The number of input and output ports must be equal and non-zero.
+     * @return the list of MidiReceivers
+     */
+    public final @NonNull List<MidiReceiver> getOutputPortReceivers() {
+        if (mServer == null) {
+            return new ArrayList<MidiReceiver>();
+        } else {
+            return Arrays.asList(mServer.getOutputPortReceivers());
+        }
+    }
+
+    /**
+     * Returns the {@link MidiDeviceInfo} instance for this service
+     * @return the MidiDeviceInfo of the virtual MIDI device
+     */
+    public final @Nullable MidiDeviceInfo getDeviceInfo() {
+        return mDeviceInfo;
+    }
+
+    /**
+     * Called to notify when the {@link MidiDeviceStatus} has changed
+     * @param status the current status of the MIDI device
+     */
+    public void onDeviceStatusChanged(@Nullable MidiDeviceStatus status) {
+    }
+
+    /**
+     * Called to notify when the virtual MIDI device running in this service has been closed by
+     * all its clients
+     */
+    public void onClose() {
+    }
+
+    @Override
+    public @Nullable IBinder onBind(@NonNull Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {
+            return mServer.getBinderInterface().asBinder();
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html
index 67df1b2..d9e38e2 100644
--- a/media/java/android/media/midi/package.html
+++ b/media/java/android/media/midi/package.html
@@ -267,6 +267,56 @@
 contain System Real-Time messages, which can be interleaved inside other
 messages.</p>
 
+<h1 id=using_midi_btle>Using MIDI Over Bluetooth LE</h1>
+
+<p>MIDI devices can be connected to Android using Bluetooth LE.</p>
+
+<p>Before using the device, the app must scan for available BTLE devices and then allow
+the user to connect.
+See the Android developer website for an
+<a href="https://source.android.com/devices/audio/midi_test#apps" target="_blank">example
+program</a>.</p>
+
+<h2 id=btle_location_permissions>Request Location Permission for BTLE</h2>
+
+<p>Applications that scan for Bluetooth devices must request permission in the
+manifest file. This LOCATION permission is required because it may be possible to
+guess the location of an Android device by seeing which BTLE devices are nearby.</p>
+
+<pre class=prettyprint>
+&lt;uses-permission android:name="android.permission.BLUETOOTH"/>
+&lt;uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+&lt;uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+</pre>
+
+<p>Apps must also request location permission from the user at run-time.
+See the documentation for <code>Activity.requestPermissions()</code> for details and an example.
+</p>
+
+<h2 id=btle_scan_devices>Scan for MIDI Devices</h2>
+
+<p>The app will only want to see MIDI devices and not mice or other non-MIDI devices.
+So construct a ScanFilter using the UUID for standard MIDI over BTLE.</p>
+
+<pre class=prettyprint>
+MIDI over BTLE UUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
+</pre>
+
+<h2 id=btle_open_device>Open a MIDI Bluetooth Device</h2>
+
+<p>See the documentation for <code>android.bluetooth.le.BluetoothLeScanner.startScan()</code>
+method for details. When the user selects a MIDI/BTLE device then you can open it
+using the MidiManager.</p>
+
+<pre class=prettyprint>
+m.openBluetoothDevice(bluetoothDevice, callback, handler);
+</pre>
+
+<p>Once the MIDI/BTLE device has been opened by one app then it will also become available to other
+apps using the
+<a href="#get_list_of_already_plugged_in_entities">MIDI device discovery calls described above</a>.
+</p>
+
 <h1 id=creating_a_midi_virtual_device_service>Creating a MIDI Virtual Device Service</h1>
 
 
@@ -355,62 +405,17 @@
 }
 </pre>
 
-<h1 id=using_midi_btle>Using MIDI Over Bluetooth LE</h1>
 
-<p>MIDI devices can be connected to Android using Bluetooth LE.</p>
+<h1 id=using_midi_2_0>Using MIDI 2.0</h1>
 
-<p>Before using the device, the app must scan for available BTLE devices and then allow
-the user to connect.
-See the Android developer website for an
-<a href="https://source.android.com/devices/audio/midi_test#apps" target="_blank">example
-program</a>.</p>
-
-<h2 id=btle_location_permissions>Request Location Permission for BTLE</h2>
-
-<p>Applications that scan for Bluetooth devices must request permission in the
-manifest file. This LOCATION permission is required because it may be possible to
-guess the location of an Android device by seeing which BTLE devices are nearby.</p>
-
-<pre class=prettyprint>
-&lt;uses-permission android:name="android.permission.BLUETOOTH"/>
-&lt;uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-&lt;uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-</pre>
-
-<p>Apps must also request location permission from the user at run-time.
-See the documentation for <code>Activity.requestPermissions()</code> for details and an example.
-</p>
-
-<h2 id=btle_scan_devices>Scan for MIDI Devices</h2>
-
-<p>The app will only want to see MIDI devices and not mice or other non-MIDI devices.
-So construct a ScanFilter using the UUID for standard MIDI over BTLE.</p>
-
-<pre class=prettyprint>
-MIDI over BTLE UUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
-</pre>
-
-<h2 id=btle_open_device>Open a MIDI Bluetooth Device</h2>
-
-<p>See the documentation for <code>android.bluetooth.le.BluetoothLeScanner.startScan()</code>
-method for details. When the user selects a MIDI/BTLE device then you can open it
-using the MidiManager.</p>
-
-<pre class=prettyprint>
-m.openBluetoothDevice(bluetoothDevice, callback, handler);
-</pre>
-
-<p>Once the MIDI/BTLE device has been opened by one app then it will also become available to other
-apps using the
-<a href="#get_list_of_already_plugged_in_entities">MIDI device discovery calls described above</a>.
-</p>
-
-<h1 id=using_midi_2_0_over_usb>Using MIDI 2.0 over USB</h1>
-
-<p>An app can use MIDI 2.0 over USB starting in Android T. MIDI 2.0 packets are embedded in
+<p>An app can use <a href=
+"https://www.midi.org/midi-articles/details-about-midi-2-0-midi-ci-profiles-and-property-exchange"
+class="external">MIDI 2.0</a> over USB starting in Android T. MIDI 2.0 packets are embedded in
 Universal MIDI Packets, or UMP for short. A MIDI 2.0 USB device should create two interfaces,
 one endpoint that accepts only MIDI 1.0 packets and one that accepts only UMP packets.
-For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 USB spec.</p>
+For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 and UMP spec. Starting from Android
+V, apps can also open <a href="#creating_a_midi_2_0_virtual_device_service"> MIDI 2.0 virtual</a>
+devices.</p>
 
 <p>MidiManager.getDevices() would simply return the 1.0 interface. This interface should work
 exactly the same as before. In order to use the new UMP interface, retrieve the device with the
@@ -421,15 +426,15 @@
         MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS);
 </pre>
 
-<p>UMP Packets are always in multiple of 4 bytes. For each set of 4 bytes, they are sent in network
-order. Compare the following NoteOn code snippet with the NoteOn code snippet above. </p>
+<p>UMP packet sizes are always a multiple of 4 bytes. For each set of 4 bytes, they are sent in
+network order. Compare the following NoteOn code snippet with the NoteOn code snippet above. </p>
 
 <pre class=prettyprint>
 byte[] buffer = new byte[32];
 int numBytes = 0;
 int channel = 3; // MIDI channels 1-16 are encoded as 0-15.
 int group = 0;
-buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 voice message
+buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 Channel Voice Message
 buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on
 buffer[numBytes++] = (byte)60; // pitch is middle C
 buffer[numBytes++] = (byte)127; // max velocity
@@ -446,5 +451,108 @@
 int defaultProtocol = info.getDefaultProtocol();
 </pre>
 
+<h1 id=creating_a_midi_2_0_virtual_device_service>Creating a MIDI 2.0 Virtual Device Service</h1>
+
+
+<p>Starting in Android V, an app can provide a MIDI 2.0 Service that can be used by other apps.
+MIDI 2.0 packets are embedded in Universal MIDI Packets, or UMP for short. The service must be
+guarded with permission &quot;android.permission.BIND_MIDI_DEVICE_SERVICE&quot;.</p>
+
+<h2 id=manifest_files>Manifest Files</h2>
+
+
+<p>An app declares that it will function as a MIDI server in the AndroidManifest.xml file. Unlike
+MIDI 1.0 virtual devices, android.media.midi.MidiUmpDeviceService is used</p>
+
+<pre class=prettyprint>
+&lt;service android:name="<strong>MidiEchoDeviceService</strong>"
+  android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
+  &lt;intent-filter>
+    &lt;action android:name="android.media.midi.MidiUmpDeviceService" />
+  &lt;/intent-filter>
+  &lt;meta-data android:name="android.media.midi.MidiUmpDeviceService"
+      android:resource="@xml/<strong>echo_device_info</strong>" />
+&lt;/service>
+</pre>
+
+
+<p>The details of the resource in this example is stored in &ldquo;res/xml/echo_device_info.xml
+&rdquo;. The port names that you declare in this file will be available from PortInfo.getName().
+Unlike MIDI 1.0, MIDI 2.0 ports are bidirectional. If you declare a port in this service, then it
+automatically creates an input port and an output port with the same name. Clients can use those
+two ports like the MIDI 1.0 ports.</p>
+
+<pre class=prettyprint>
+&lt;devices>
+    &lt;device manufacturer="MyCompany" product="MidiEcho">
+        &lt;port name="port1" />
+    &lt;/device>
+&lt;/devices>
+</pre>
+
+
+<h2 id=extend_midiumpdeviceservice>Extend MidiUmpDeviceService</h2>
+
+
+<p>You then define your server by extending android.media.midi.MidiUmpDeviceService.</p>
+
+<pre class=prettyprint>
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiUmpDeviceService;
+
+public class MidiEchoDeviceService extends MidiUmpDeviceService {
+    private static final String TAG = "MidiEchoDeviceService";
+    // Other apps will write to this port.
+    private MidiReceiver mInputReceiver = new MyReceiver();
+    // This app will copy the data to this port.
+    private MidiReceiver mOutputReceiver;
+
+    &#64;Override
+    public void onCreate() {
+        super.onCreate();
+    }
+
+    &#64;Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+    &#64;Override
+    // Declare the receivers associated with your input ports.
+    public List<MidiReceiver> onGetInputPortReceivers() {
+        return new ArrayList<MidiReceiver>(Collections.singletonList(mInputReceiver));
+    }
+
+    /**
+     * Sample receiver to echo from the input port to the output port.
+     * In this example, we are just echoing the data and not parsing it.
+     * You will probably want to convert the bytes to a packet and then interpret the packet.
+     * See the MIDI 2.0 spec at the MMA site. Packets are either 4, 8, 12 or 16 bytes.
+     */
+    class MyReceiver extends MidiReceiver {
+        &#64;Override
+        public void onSend(byte[] data, int offset, int count, long timestamp)
+                throws IOException {
+            if (mOutputReceiver == null) {
+                mOutputReceiver = getOutputPortReceivers().get(0);
+            }
+            // Copy input to output.
+            mOutputReceiver.send(data, offset, count, timestamp);
+        }
+    }
+
+    /**
+     * This will get called when clients connect or disconnect.
+     * You can use it to figure out how many devices are connected.
+     */
+    &#64;Override
+    public void onDeviceStatusChanged(MidiDeviceStatus status) {
+        // inputOpened = status.isInputPortOpen(0);
+        // outputOpenCount = status.getOutputPortOpenCount(0);
+    }
+}
+</pre>
+
 </body>
 </html>
diff --git a/native/android/configuration.cpp b/native/android/configuration.cpp
index b50514d..283445f 100644
--- a/native/android/configuration.cpp
+++ b/native/android/configuration.cpp
@@ -36,7 +36,7 @@
 
 void AConfiguration_fromAssetManager(AConfiguration* out, AAssetManager* am) {
     ScopedLock<AssetManager2> locked_mgr(*AssetManagerForNdkAssetManager(am));
-    ResTable_config config = locked_mgr->GetConfiguration();
+    ResTable_config config = locked_mgr->GetConfigurations()[0];
 
     // AConfiguration is not a virtual subclass, so we can memcpy.
     memcpy(out, &config, sizeof(config));
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 3adb882..3252aad 100644
--- a/packages/SettingsLib/res/values/arrays.xml
+++ b/packages/SettingsLib/res/values/arrays.xml
@@ -660,4 +660,20 @@
         <item>2.0</item>
     </string-array>
 
+    <!-- Grammatical genders -->
+    <array name="grammatical_gender_entries">
+        <item>@string/not_specified</item>
+        <item>@string/neuter</item>
+        <item>@string/feminine</item>
+        <item>@string/masculine</item>
+    </array>
+
+    <!-- Values for grammatical genders -->
+    <string-array name="grammatical_gender_values">
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+        <item>3</item>
+    </string-array>
+
 </resources>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 60bc226..dab3bcb 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1678,4 +1678,13 @@
 
     <!-- Formatting states for the scale of font size, in percent. Double "%" is required to represent the symbol "%". [CHAR LIMIT=20] -->
     <string name="font_scale_percentage"> <xliff:g id="percentage">%1$d</xliff:g> %%</string>
+
+    <!-- List entry in developer settings to set the grammatical gender to Not specified [CHAR LIMIT=30]-->
+    <string name="not_specified">Not specified</string>
+    <!-- List entry in developer settings to set the grammatical gender to Neuter [CHAR LIMIT=30]-->
+    <string name="neuter">Neuter</string>
+    <!-- List entry in developer settings to set the grammatical gender to Feminine [CHAR LIMIT=30]-->
+    <string name="feminine">Feminine</string>
+    <!-- List entry in developer settings to set the grammatical gender to Masculine [CHAR LIMIT=30]-->
+    <string name="masculine">Masculine</string>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 441d3a5..a6536a8c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -29,6 +29,7 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -365,16 +366,17 @@
     public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
         device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
         CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
-        final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
+        // Should iterate through the cloned set to avoid ConcurrentModificationException
+        final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice());
         if (!memberDevices.isEmpty()) {
-            // Main device is unpaired, to unpair the member device
+            // Main device is unpaired, also unpair the member devices
             for (CachedBluetoothDevice memberDevice : memberDevices) {
                 memberDevice.unpair();
                 memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
                 device.removeMemberDevice(memberDevice);
             }
         } else if (mainDevice != null) {
-            // the member device unpaired, to unpair main device
+            // Member device is unpaired, also unpair the main device
             mainDevice.unpair();
         }
         mainDevice = mHearingAidDeviceManager.findMainDevice(device);
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 7a5a382..c59b0f9 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -72,7 +72,7 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         },
         {
           "include-filter": "android.permissionui.cts.CameraMicIndicatorsPermissionTest"
@@ -110,6 +110,17 @@
       ]
     }
   ],
+  "postsubmit": [
+    {
+      // Permission indicators
+      "name": "CtsPermissionUiTestCases",
+      "options": [
+        {
+          "include-filter": "android.permissionui.cts.CameraMicIndicatorsPermissionTest"
+        }
+      ]
+    }
+  ],
   "silver-sysui": [
    {
       "name": "PlatformScenarioTests",
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
new file mode 100644
index 0000000..566967f
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.DisposableEffectResult
+import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.lerp
+import com.android.compose.ui.util.lerp
+
+/**
+ * Animate a shared Int value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedIntAsState(
+    value: Int,
+    key: ValueKey,
+    element: ElementKey,
+    canOverflow: Boolean = true,
+): State<Int> {
+    return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Float value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedFloatAsState(
+    value: Float,
+    key: ValueKey,
+    element: ElementKey,
+    canOverflow: Boolean = true,
+): State<Float> {
+    return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Dp value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedDpAsState(
+    value: Dp,
+    key: ValueKey,
+    element: ElementKey,
+    canOverflow: Boolean = true,
+): State<Dp> {
+    return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Color value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedColorAsState(
+    value: Color,
+    key: ValueKey,
+    element: ElementKey,
+): State<Color> {
+    return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
+}
+
+@Composable
+internal fun <T> animateSharedValueAsState(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+    key: ValueKey,
+    value: T,
+    lerp: (T, T, Float) -> T,
+    canOverflow: Boolean,
+): State<T> {
+    val sharedValue = remember(key) { Element.SharedValue(key, value) }
+    if (value != sharedValue.value) {
+        sharedValue.value = value
+    }
+
+    DisposableEffect(element, scene, sharedValue) {
+        addSharedValueToElement(element, scene, sharedValue)
+    }
+
+    return remember(layoutImpl, element, sharedValue, lerp, canOverflow) {
+        derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) }
+    }
+}
+
+private fun <T> DisposableEffectScope.addSharedValueToElement(
+    element: Element,
+    scene: Scene,
+    sharedValue: Element.SharedValue<T>,
+): DisposableEffectResult {
+    val sceneValues =
+        element.sceneValues[scene.key] ?: error("Element $element is not present in $scene")
+    val sharedValues = sceneValues.sharedValues
+
+    sharedValues[sharedValue.key] = sharedValue
+    return onDispose { sharedValues.remove(sharedValue.key) }
+}
+
+private fun <T> computeValue(
+    layoutImpl: SceneTransitionLayoutImpl,
+    element: Element,
+    sharedValue: Element.SharedValue<T>,
+    lerp: (T, T, Float) -> T,
+    canOverflow: Boolean,
+): T {
+    val state = layoutImpl.state.transitionState
+    if (
+        state !is TransitionState.Transition ||
+            state.fromScene == state.toScene ||
+            !layoutImpl.isTransitionReady(state)
+    ) {
+        return sharedValue.value
+    }
+
+    fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
+        val sceneValues = element.sceneValues[scene] ?: return null
+        val value = sceneValues.sharedValues[sharedValue.key] ?: return null
+        return value as Element.SharedValue<T>
+    }
+
+    val fromValue = sceneValue(state.fromScene)
+    val toValue = sceneValue(state.toScene)
+    return if (fromValue != null && toValue != null) {
+        val progress = if (canOverflow) state.progress else state.progress.coerceIn(0f, 1f)
+        lerp(fromValue.value, toValue.value, progress)
+    } else if (fromValue != null) {
+        fromValue.value
+    } else if (toValue != null) {
+        toValue.value
+    } else {
+        sharedValue.value
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
new file mode 100644
index 0000000..7536728
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.SpringSpec
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Transition to [target] using a canned animation. This function will try to be smart and take over
+ * the currently running transition, if there is one.
+ */
+internal fun CoroutineScope.animateToScene(
+    layoutImpl: SceneTransitionLayoutImpl,
+    target: SceneKey,
+) {
+    val state = layoutImpl.state.transitionState
+    if (state.currentScene == target) {
+        // This can happen in 3 different situations, for which there isn't anything else to do:
+        //  1. There is no ongoing transition and [target] is already the current scene.
+        //  2. The user is swiping to [target] from another scene and released their pointer such
+        //     that the gesture was committed and the transition is animating to [scene] already.
+        //  3. The user is swiping from [target] to another scene and either:
+        //     a. didn't release their pointer yet.
+        //     b. released their pointer such that the swipe gesture was cancelled and the
+        //        transition is currently animating back to [target].
+        return
+    }
+
+    when (state) {
+        is TransitionState.Idle -> animate(layoutImpl, target)
+        is TransitionState.Transition -> {
+            if (state.toScene == state.fromScene) {
+                // Same as idle.
+                animate(layoutImpl, target)
+                return
+            }
+
+            // A transition is currently running: first check whether `transition.toScene` or
+            // `transition.fromScene` is the same as our target scene, in which case the transition
+            // can be accelerated or reversed to end up in the target state.
+
+            if (state.toScene == target) {
+                // The user is currently swiping to [target] but didn't release their pointer yet:
+                // animate the progress to `1`.
+
+                check(state.fromScene == state.currentScene)
+                val progress = state.progress
+                if ((1f - progress).absoluteValue < ProgressVisibilityThreshold) {
+                    // The transition is already finished (progress ~= 1): no need to animate.
+                    layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+                } else {
+                    // The transition is in progress: start the canned animation at the same
+                    // progress as it was in.
+                    // TODO(b/290184746): Also take the current velocity into account.
+                    animate(layoutImpl, target, startProgress = progress)
+                }
+
+                return
+            }
+
+            if (state.fromScene == target) {
+                // There is a transition from [target] to another scene: simply animate the same
+                // transition progress to `0`.
+
+                check(state.toScene == state.currentScene)
+                val progress = state.progress
+                if (progress.absoluteValue < ProgressVisibilityThreshold) {
+                    // The transition is at progress ~= 0: no need to animate.
+                    layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+                } else {
+                    // TODO(b/290184746): Also take the current velocity into account.
+                    animate(layoutImpl, target, startProgress = progress, reversed = true)
+                }
+
+                return
+            }
+
+            // Generic interruption; the current transition is neither from or to [target].
+            // TODO(b/290930950): Better handle interruptions here.
+            animate(layoutImpl, target)
+        }
+    }
+}
+
+private fun CoroutineScope.animate(
+    layoutImpl: SceneTransitionLayoutImpl,
+    target: SceneKey,
+    startProgress: Float = 0f,
+    reversed: Boolean = false,
+) {
+    val fromScene = layoutImpl.state.transitionState.currentScene
+
+    val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
+    val visibilityThreshold =
+        (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
+    val animatable = Animatable(startProgress, visibilityThreshold = visibilityThreshold)
+
+    val targetProgress = if (reversed) 0f else 1f
+    val transition =
+        if (reversed) {
+            OneOffTransition(target, fromScene, currentScene = target, animatable)
+        } else {
+            OneOffTransition(fromScene, target, currentScene = target, animatable)
+        }
+
+    // Change the current layout state to use this new transition.
+    layoutImpl.state.transitionState = transition
+
+    // Animate the progress to its target value.
+    launch {
+        animatable.animateTo(targetProgress, animationSpec)
+
+        // Unless some other external state change happened, the state should now be idle.
+        if (layoutImpl.state.transitionState == transition) {
+            layoutImpl.state.transitionState = TransitionState.Idle(target)
+        }
+    }
+}
+
+private class OneOffTransition(
+    override val fromScene: SceneKey,
+    override val toScene: SceneKey,
+    override val currentScene: SceneKey,
+    private val animatable: Animatable<Float, AnimationVector1D>,
+) : TransitionState.Transition {
+    override val progress: Float
+        get() = animatable.value
+}
+
+// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
+// and screen density.
+private const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
new file mode 100644
index 0000000..0cc259a
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
@@ -0,0 +1,449 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.IntermediateMeasureScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.util.lerp
+
+/** An element on screen, that can be composed in one or more scenes. */
+internal class Element(val key: ElementKey) {
+    /**
+     * The last offset assigned to this element, relative to the SceneTransitionLayout containing
+     * it.
+     */
+    var lastOffset = Offset.Unspecified
+
+    /** The last size assigned to this element. */
+    var lastSize = SizeUnspecified
+
+    /** The last alpha assigned to this element. */
+    var lastAlpha = 1f
+
+    /** The mapping between a scene and the values/state this element has in that scene, if any. */
+    val sceneValues = SnapshotStateMap<SceneKey, SceneValues>()
+
+    override fun toString(): String {
+        return "Element(key=$key)"
+    }
+
+    /** The target values of this element in a given scene. */
+    class SceneValues {
+        var size by mutableStateOf(SizeUnspecified)
+        var offset by mutableStateOf(Offset.Unspecified)
+        val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
+    }
+
+    /** A shared value of this element. */
+    class SharedValue<T>(val key: ValueKey, initialValue: T) {
+        var value by mutableStateOf(initialValue)
+    }
+
+    companion object {
+        val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
+    }
+}
+
+/** The implementation of [SceneScope.element]. */
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.element(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    key: ElementKey,
+): Modifier {
+    val sceneValues = remember(scene, key) { Element.SceneValues() }
+    val element =
+        // Get the element associated to [key] if it was already composed in another scene,
+        // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
+        // withoutReadObservation() because there is no need to recompose when that map is mutated.
+        Snapshot.withoutReadObservation {
+            val element =
+                layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+            val previousValues = element.sceneValues[scene.key]
+            if (previousValues == null) {
+                element.sceneValues[scene.key] = sceneValues
+            } else if (previousValues != sceneValues) {
+                error("$key was composed multiple times in $scene")
+            }
+
+            element
+        }
+
+    DisposableEffect(scene, sceneValues, element) {
+        onDispose {
+            element.sceneValues.remove(scene.key)
+
+            // This was the last scene this element was in, so remove it from the map.
+            if (element.sceneValues.isEmpty()) {
+                layoutImpl.elements.remove(element.key)
+            }
+        }
+    }
+
+    val alpha =
+        remember(layoutImpl, element, scene) {
+            derivedStateOf { elementAlpha(layoutImpl, element, scene) }
+        }
+    val isOpaque by remember(alpha) { derivedStateOf { alpha.value == 1f } }
+    SideEffect {
+        if (isOpaque && element.lastAlpha != 1f) {
+            element.lastAlpha = 1f
+        }
+    }
+
+    return drawWithContent {
+            if (shouldDrawElement(layoutImpl, scene, element)) {
+                drawContent()
+            }
+        }
+        .modifierTransformations(layoutImpl, scene, element, sceneValues)
+        .intermediateLayout { measurable, constraints ->
+            val placeable =
+                measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
+            layout(placeable.width, placeable.height) {
+                place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
+            }
+        }
+        .thenIf(!isOpaque) {
+            Modifier.graphicsLayer {
+                val alpha = alpha.value
+                this.alpha = alpha
+                element.lastAlpha = alpha
+            }
+        }
+        .testTag(key.name)
+}
+
+private fun shouldDrawElement(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+): Boolean {
+    val state = layoutImpl.state.transitionState
+
+    // Always draw the element if there is no ongoing transition or if the element is not shared.
+    if (
+        state !is TransitionState.Transition ||
+            state.fromScene == state.toScene ||
+            !layoutImpl.isTransitionReady(state) ||
+            state.fromScene !in element.sceneValues ||
+            state.toScene !in element.sceneValues
+    ) {
+        return true
+    }
+
+    val otherScene =
+        layoutImpl.scenes.getValue(
+            if (scene.key == state.fromScene) {
+                state.toScene
+            } else {
+                state.fromScene
+            }
+        )
+
+    // When the element is shared, draw the one in the highest scene unless it is a background, i.e.
+    // it is usually drawn below everything else.
+    val isHighestScene = scene.zIndex > otherScene.zIndex
+    return if (element.key.isBackground) {
+        !isHighestScene
+    } else {
+        isHighestScene
+    }
+}
+
+/**
+ * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
+ * throughout the current transition, if any.
+ */
+private fun Modifier.modifierTransformations(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+    sceneValues: Element.SceneValues,
+): Modifier {
+    when (val state = layoutImpl.state.transitionState) {
+        is TransitionState.Idle -> return this
+        is TransitionState.Transition -> {
+            val fromScene = state.fromScene
+            val toScene = state.toScene
+            if (fromScene == toScene) {
+                // Same as idle.
+                return this
+            }
+
+            return layoutImpl.transitions
+                .transitionSpec(fromScene, state.toScene)
+                .transformations(element.key)
+                .modifier
+                .fold(this) { modifier, transformation ->
+                    with(transformation) {
+                        modifier.transform(layoutImpl, scene, element, sceneValues)
+                    }
+                }
+        }
+    }
+}
+
+private fun elementAlpha(
+    layoutImpl: SceneTransitionLayoutImpl,
+    element: Element,
+    scene: Scene
+): Float {
+    return computeValue(
+            layoutImpl,
+            scene,
+            element,
+            sceneValue = { 1f },
+            transformation = { it.alpha },
+            idleValue = 1f,
+            currentValue = { 1f },
+            lastValue = { element.lastAlpha },
+            ::lerp,
+        )
+        .coerceIn(0f, 1f)
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.measure(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+    sceneValues: Element.SceneValues,
+    measurable: Measurable,
+    constraints: Constraints,
+): Placeable {
+    // Update the size this element has in this scene when idle.
+    val targetSizeInScene = lookaheadSize
+    if (targetSizeInScene != sceneValues.size) {
+        // TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
+        sceneValues.size = targetSizeInScene
+    }
+
+    // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
+    // case we store the resulting placeable here to make sure the element is not measured more than
+    // once.
+    var maybePlaceable: Placeable? = null
+
+    fun Placeable.size() = IntSize(width, height)
+
+    val targetSize =
+        computeValue(
+            layoutImpl,
+            scene,
+            element,
+            sceneValue = { it.size },
+            transformation = { it.size },
+            idleValue = lookaheadSize,
+            currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
+            lastValue = {
+                val lastSize = element.lastSize
+                if (lastSize != Element.SizeUnspecified) {
+                    lastSize
+                } else {
+                    measurable.measure(constraints).also { maybePlaceable = it }.size()
+                }
+            },
+            ::lerp,
+        )
+
+    val placeable =
+        maybePlaceable
+            ?: measurable.measure(
+                Constraints.fixed(
+                    targetSize.width.coerceAtLeast(0),
+                    targetSize.height.coerceAtLeast(0),
+                )
+            )
+
+    element.lastSize = placeable.size()
+    return placeable
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.place(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+    sceneValues: Element.SceneValues,
+    placeable: Placeable,
+    placementScope: Placeable.PlacementScope,
+) {
+    with(placementScope) {
+        // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
+        // when idle.
+        val coords = coordinates!!
+        val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
+        if (targetOffsetInScene != sceneValues.offset) {
+            // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
+            sceneValues.offset = targetOffsetInScene
+        }
+
+        val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
+        val targetOffset =
+            computeValue(
+                layoutImpl,
+                scene,
+                element,
+                sceneValue = { it.offset },
+                transformation = { it.offset },
+                idleValue = targetOffsetInScene,
+                currentValue = { currentOffset },
+                lastValue = {
+                    val lastValue = element.lastOffset
+                    if (lastValue.isSpecified) {
+                        lastValue
+                    } else {
+                        currentOffset
+                    }
+                },
+                ::lerp,
+            )
+
+        element.lastOffset = targetOffset
+        placeable.place((targetOffset - currentOffset).round())
+    }
+}
+
+/**
+ * Return the value that should be used depending on the current layout state and transition.
+ *
+ * Important: This function must remain inline because of all the lambda parameters. These lambdas
+ * are necessary because getting some of them might require some computation, like measuring a
+ * Measurable.
+ *
+ * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
+ * @param scene the scene containing [element].
+ * @param element the element being animated.
+ * @param sceneValue the value being animated.
+ * @param transformation the transformation associated to the value being animated.
+ * @param idleValue the value when idle, i.e. when there is no transition happening.
+ * @param currentValue the value that would be used if it is not transformed. Note that this is
+ *   different than [idleValue] even if the value is not transformed directly because it could be
+ *   impacted by the transformations on other elements, like a parent that is being translated or
+ *   resized.
+ * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
+ *   the first time the value is set.
+ * @param lerp the linear interpolation function used to interpolate between two values of this
+ *   value type.
+ */
+private inline fun <T> computeValue(
+    layoutImpl: SceneTransitionLayoutImpl,
+    scene: Scene,
+    element: Element,
+    sceneValue: (Element.SceneValues) -> T,
+    transformation: (ElementTransformations) -> PropertyTransformation<T>?,
+    idleValue: T,
+    currentValue: () -> T,
+    lastValue: () -> T,
+    lerp: (T, T, Float) -> T,
+): T {
+    val state = layoutImpl.state.transitionState
+
+    // There is no ongoing transition.
+    if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+        return idleValue
+    }
+
+    // A transition was started but it's not ready yet (not all elements have been composed/laid
+    // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
+    if (!layoutImpl.isTransitionReady(state)) {
+        return lastValue()
+    }
+
+    val fromScene = state.fromScene
+    val toScene = state.toScene
+    val fromValues = element.sceneValues[fromScene]
+    val toValues = element.sceneValues[toScene]
+
+    if (fromValues == null && toValues == null) {
+        error("This should not happen, element $element is neither in $fromScene or $toScene")
+    }
+
+    // TODO(b/291053278): Handle overscroll correctly. We should probably coerce between [0f, 1f]
+    // here and consume overflows at drawing time, somehow reusing Compose OverflowEffect or some
+    // similar mechanism.
+    val transitionProgress = state.progress
+
+    // The element is shared: interpolate between the value in fromScene and the value in toScene.
+    // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
+    // elements follow the finger direction.
+    if (fromValues != null && toValues != null) {
+        return lerp(
+            sceneValue(fromValues),
+            sceneValue(toValues),
+            transitionProgress,
+        )
+    }
+
+    val transformation =
+        transformation(
+            layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
+        )
+        // If there is no transformation explicitly associated to this element value, let's use
+        // the value given by the system (like the current position and size given by the layout
+        // pass).
+        ?: return currentValue()
+
+    // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
+    // end (for leaving elements) of the transition.
+    val targetValue =
+        transformation.transform(
+            layoutImpl,
+            scene,
+            element,
+            fromValues ?: toValues!!,
+            state,
+            idleValue,
+        )
+
+    // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
+    val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress
+
+    // Interpolate between the value at rest and the value before entering/after leaving.
+    val isEntering = fromValues == null
+    return if (isEntering) {
+        lerp(targetValue, idleValue, rangeProgress)
+    } else {
+        lerp(idleValue, targetValue, rangeProgress)
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
new file mode 100644
index 0000000..c3f44f8
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+/**
+ * A base class to create unique keys, associated to an [identity] that is used to check the
+ * equality of two key instances.
+ */
+sealed class Key(val name: String, val identity: Any) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (this.javaClass != other?.javaClass) return false
+        return identity == (other as? Key)?.identity
+    }
+
+    override fun hashCode(): Int {
+        return identity.hashCode()
+    }
+
+    override fun toString(): String {
+        return "Key(name=$name)"
+    }
+}
+
+/** Key for a scene. */
+class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) {
+    override fun toString(): String {
+        return "SceneKey(name=$name)"
+    }
+}
+
+/** Key for an element. */
+class ElementKey(
+    name: String,
+    identity: Any = Object(),
+
+    /**
+     * Whether this element is a background and usually drawn below other elements. This should be
+     * set to true to make sure that shared backgrounds are drawn below elements of other scenes.
+     */
+    val isBackground: Boolean = false,
+) : Key(name, identity), ElementMatcher {
+    override fun matches(key: ElementKey): Boolean {
+        return key == this
+    }
+
+    override fun toString(): String {
+        return "ElementKey(name=$name)"
+    }
+
+    companion object {
+        /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
+        fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
+            return object : ElementMatcher {
+                override fun matches(key: ElementKey): Boolean = predicate(key.identity)
+            }
+        }
+    }
+}
+
+/** Key for a shared value of an element. */
+class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+    override fun toString(): String {
+        return "ValueKey(name=$name)"
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
new file mode 100644
index 0000000..a625250
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * A scene transition state.
+ *
+ * This models the same thing as [TransitionState], with the following distinctions:
+ * 1. [TransitionState] values are backed by the Snapshot system (Compose State objects) and can be
+ *    used by callers tracking State reads, for instance in Compose code during the composition,
+ *    layout or Compose drawing phases.
+ * 2. [ObservableTransitionState] values are backed by Kotlin [Flow]s and can be collected by
+ *    non-Compose code to observe value changes.
+ * 3. [ObservableTransitionState.Transition.fromScene] and
+ *    [ObservableTransitionState.Transition.toScene] will never be equal, while
+ *    [TransitionState.Transition.fromScene] and [TransitionState.Transition.toScene] can be equal.
+ */
+sealed class ObservableTransitionState {
+    /** No transition/animation is currently running. */
+    data class Idle(val scene: SceneKey) : ObservableTransitionState()
+
+    /** There is a transition animating between two scenes. */
+    data class Transition(
+        val fromScene: SceneKey,
+        val toScene: SceneKey,
+        val progress: Flow<Float>,
+    ) : ObservableTransitionState()
+}
+
+/**
+ * The current [ObservableTransitionState]. This models the same thing as
+ * [SceneTransitionLayoutState.transitionState], except that it is backed by Flows and can be used
+ * by non-Compose code to observe state changes.
+ */
+fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTransitionState> {
+    return snapshotFlow {
+            when (val state = transitionState) {
+                is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene)
+                is TransitionState.Transition -> {
+                    if (state.fromScene == state.toScene) {
+                        ObservableTransitionState.Idle(state.currentScene)
+                    } else {
+                        ObservableTransitionState.Transition(
+                            fromScene = state.fromScene,
+                            toScene = state.toScene,
+                            progress = snapshotFlow { state.progress },
+                        )
+                    }
+                }
+            }
+        }
+        .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
new file mode 100644
index 0000000..b44c8ef
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.zIndex
+
+/** A scene in a [SceneTransitionLayout]. */
+internal class Scene(
+    val key: SceneKey,
+    layoutImpl: SceneTransitionLayoutImpl,
+    content: @Composable SceneScope.() -> Unit,
+    actions: Map<UserAction, SceneKey>,
+    zIndex: Float,
+) {
+    private val scope = SceneScopeImpl(layoutImpl, this)
+
+    var content by mutableStateOf(content)
+    var userActions by mutableStateOf(actions)
+    var zIndex by mutableFloatStateOf(zIndex)
+    var size by mutableStateOf(IntSize.Zero)
+
+    @Composable
+    fun Content(modifier: Modifier = Modifier) {
+        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }) { scope.content() }
+    }
+
+    override fun toString(): String {
+        return "Scene(key=$key)"
+    }
+}
+
+private class SceneScopeImpl(
+    private val layoutImpl: SceneTransitionLayoutImpl,
+    private val scene: Scene,
+) : SceneScope {
+    @Composable
+    override fun Modifier.element(key: ElementKey): Modifier {
+        return element(layoutImpl, scene, key)
+    }
+
+    @Composable
+    override fun <T> animateSharedValueAsState(
+        value: T,
+        key: ValueKey,
+        element: ElementKey,
+        lerp: (T, T, Float) -> T,
+        canOverflow: Boolean
+    ): State<T> {
+        val element =
+            layoutImpl.elements[element]
+                ?: error(
+                    "Element $element is not composed. Make sure to call animateSharedXAsState " +
+                        "*after* Modifier.element(key)."
+                )
+
+        return animateSharedValueAsState(
+            layoutImpl,
+            scene,
+            element,
+            key,
+            value,
+            lerp,
+            canOverflow,
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
new file mode 100644
index 0000000..39173d9
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+
+/**
+ * [SceneTransitionLayout] is a container that automatically animates its content whenever
+ * [currentScene] changes, using the transitions defined in [transitions].
+ *
+ * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
+ * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
+ * you need support for swipe gestures, shared elements or transitions defined declaratively outside
+ * UI code.
+ *
+ * @param currentScene the current scene
+ * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
+ *   This is called when the user commits a transition to a new scene because of a [UserAction], for
+ *   instance by triggering back navigation or by swiping to a new scene.
+ * @param transitions the definition of the transitions used to animate a change of scene.
+ * @param state the observable state of this layout.
+ * @param scenes the configuration of the different scenes of this layout.
+ */
+@Composable
+fun SceneTransitionLayout(
+    currentScene: SceneKey,
+    onChangeScene: (SceneKey) -> Unit,
+    transitions: SceneTransitions,
+    modifier: Modifier = Modifier,
+    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+    scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+    val density = LocalDensity.current
+    val layoutImpl = remember {
+        SceneTransitionLayoutImpl(
+            onChangeScene,
+            scenes,
+            transitions,
+            state,
+            density,
+        )
+    }
+
+    layoutImpl.onChangeScene = onChangeScene
+    layoutImpl.transitions = transitions
+    layoutImpl.density = density
+    layoutImpl.setScenes(scenes)
+    layoutImpl.setCurrentScene(currentScene)
+
+    layoutImpl.Content(modifier)
+}
+
+interface SceneTransitionLayoutScope {
+    /**
+     * Add a scene to this layout, identified by [key].
+     *
+     * You can configure [userActions] so that swiping on this layout or navigating back will
+     * transition to a different scene.
+     *
+     * Important: scene order along the z-axis follows call order. Calling scene(A) followed by
+     * scene(B) will mean that scene B renders after/above scene A.
+     */
+    fun scene(
+        key: SceneKey,
+        userActions: Map<UserAction, SceneKey> = emptyMap(),
+        content: @Composable SceneScope.() -> Unit,
+    )
+}
+
+interface SceneScope {
+    /**
+     * Tag an element identified by [key].
+     *
+     * Tagging an element will allow you to reference that element when defining transitions, so
+     * that the element can be transformed and animated when the scene transitions in or out.
+     *
+     * Additionally, this [key] will be used to detect elements that are shared between scenes to
+     * automatically interpolate their size, offset and [shared values][animateSharedValueAsState].
+     *
+     * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
+     *   constraint.
+     */
+    @Composable fun Modifier.element(key: ElementKey): Modifier
+
+    /**
+     * Animate some value of a shared element.
+     *
+     * @param value the value of this shared value in the current scene.
+     * @param key the key of this shared value.
+     * @param element the element associated with this value.
+     * @param lerp the *linear* interpolation function that should be used to interpolate between
+     *   two different values. Note that it has to be linear because the [fraction] passed to this
+     *   interpolator is already interpolated.
+     * @param canOverflow whether this value can overflow past the values it is interpolated
+     *   between, for instance because the transition is animated using a bouncy spring.
+     * @see animateSharedIntAsState
+     * @see animateSharedFloatAsState
+     * @see animateSharedDpAsState
+     * @see animateSharedColorAsState
+     */
+    @Composable
+    fun <T> animateSharedValueAsState(
+        value: T,
+        key: ValueKey,
+        element: ElementKey,
+        lerp: (start: T, stop: T, fraction: Float) -> T,
+        canOverflow: Boolean,
+    ): State<T>
+}
+
+/** An action performed by the user. */
+sealed interface UserAction
+
+/** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
+object Back : UserAction
+
+/** The user swiped on the container. */
+enum class Swipe : UserAction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
new file mode 100644
index 0000000..350b9c2
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.ui.util.fastForEach
+import kotlinx.coroutines.channels.Channel
+
+internal class SceneTransitionLayoutImpl(
+    onChangeScene: (SceneKey) -> Unit,
+    builder: SceneTransitionLayoutScope.() -> Unit,
+    transitions: SceneTransitions,
+    internal val state: SceneTransitionLayoutState,
+    density: Density,
+) {
+    internal val scenes = SnapshotStateMap<SceneKey, Scene>()
+    internal val elements = SnapshotStateMap<ElementKey, Element>()
+
+    /** The scenes that are "ready", i.e. they were composed and fully laid-out at least once. */
+    private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
+
+    internal var onChangeScene by mutableStateOf(onChangeScene)
+    internal var transitions by mutableStateOf(transitions)
+    internal var density: Density by mutableStateOf(density)
+
+    /**
+     * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
+     * any scene configured or right before the first measure pass of the layout.
+     */
+    internal var size by mutableStateOf(IntSize.Zero)
+
+    init {
+        setScenes(builder)
+    }
+
+    internal fun scene(key: SceneKey): Scene {
+        return scenes[key] ?: error("Scene $key is not configured")
+    }
+
+    internal fun setScenes(builder: SceneTransitionLayoutScope.() -> Unit) {
+        // Keep a reference of the current scenes. After processing [builder], the scenes that were
+        // not configured will be removed.
+        val scenesToRemove = scenes.keys.toMutableSet()
+
+        // The incrementing zIndex of each scene.
+        var zIndex = 0f
+
+        object : SceneTransitionLayoutScope {
+                override fun scene(
+                    key: SceneKey,
+                    userActions: Map<UserAction, SceneKey>,
+                    content: @Composable SceneScope.() -> Unit,
+                ) {
+                    scenesToRemove.remove(key)
+
+                    val scene = scenes[key]
+                    if (scene != null) {
+                        // Update an existing scene.
+                        scene.content = content
+                        scene.userActions = userActions
+                        scene.zIndex = zIndex
+                    } else {
+                        // New scene.
+                        scenes[key] =
+                            Scene(
+                                key,
+                                this@SceneTransitionLayoutImpl,
+                                content,
+                                userActions,
+                                zIndex,
+                            )
+                    }
+
+                    zIndex++
+                }
+            }
+            .builder()
+
+        scenesToRemove.forEach { scenes.remove(it) }
+    }
+
+    @Composable
+    internal fun setCurrentScene(key: SceneKey) {
+        val channel = remember { Channel<SceneKey>(Channel.CONFLATED) }
+        SideEffect { channel.trySend(key) }
+        LaunchedEffect(channel) {
+            for (newKey in channel) {
+                // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
+                // late.
+                val newKey = channel.tryReceive().getOrNull() ?: newKey
+                animateToScene(this@SceneTransitionLayoutImpl, newKey)
+            }
+        }
+    }
+
+    @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
+    internal fun Content(modifier: Modifier) {
+        Box(
+            modifier
+                // Handle horizontal and vertical swipes on this layout.
+                // Note: order here is important and will give a slight priority to the vertical
+                // swipes.
+                .swipeToScene(layoutImpl = this, Orientation.Horizontal)
+                .swipeToScene(layoutImpl = this, Orientation.Vertical)
+                .onSizeChanged { size = it }
+        ) {
+            LookaheadScope {
+                val scenesToCompose =
+                    when (val state = state.transitionState) {
+                        is TransitionState.Idle -> listOf(scene(state.currentScene))
+                        is TransitionState.Transition -> {
+                            if (state.toScene != state.fromScene) {
+                                listOf(scene(state.toScene), scene(state.fromScene))
+                            } else {
+                                listOf(scene(state.fromScene))
+                            }
+                        }
+                    }
+
+                // Handle back events.
+                // TODO(b/290184746): Make sure that this works with SystemUI once we use
+                // SceneTransitionLayout in Flexiglass.
+                scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
+                    BackHandler { onChangeScene(backScene) }
+                }
+
+                Box(
+                    Modifier.drawWithContent {
+                        drawContent()
+
+                        // At this point, all scenes in scenesToCompose are fully laid out so they
+                        // are marked as ready. This is necessary because the animation code needs
+                        // to know the position and size of the elements in each scenes they are in,
+                        // so [readyScenes] will be used to decide whether the transition is ready
+                        // (see isTransitionReady() below).
+                        //
+                        // We can't do that in a DisposableEffect or SideEffect because those are
+                        // run between composition and layout. LaunchedEffect could work and might
+                        // be better, but it looks like launched effects run a frame later than this
+                        // code so doing this here seems better for performance.
+                        scenesToCompose.fastForEach { readyScenes[it.key] = true }
+                    }
+                ) {
+                    scenesToCompose.fastForEach { scene ->
+                        val key = scene.key
+                        key(key) {
+                            DisposableEffect(key) { onDispose { readyScenes.remove(key) } }
+
+                            scene.Content(
+                                Modifier.drawWithContent {
+                                    when (val state = state.transitionState) {
+                                        is TransitionState.Idle -> drawContent()
+                                        is TransitionState.Transition -> {
+                                            // Don't draw scenes that are not ready yet.
+                                            if (
+                                                readyScenes.containsKey(key) ||
+                                                    state.fromScene == state.toScene
+                                            ) {
+                                                drawContent()
+                                            }
+                                        }
+                                    }
+                                }
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Return whether [transition] is ready, i.e. the elements of both scenes of the transition were
+     * laid out at least once.
+     */
+    internal fun isTransitionReady(transition: TransitionState.Transition): Boolean {
+        return readyScenes.containsKey(transition.fromScene) &&
+            readyScenes.containsKey(transition.toScene)
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
new file mode 100644
index 0000000..47e3d5a
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/** The state of a [SceneTransitionLayout]. */
+class SceneTransitionLayoutState(initialScene: SceneKey) {
+    /**
+     * The current [TransitionState]. All values read here are backed by the Snapshot system.
+     *
+     * To observe those values outside of Compose/the Snapshot system, use
+     * [SceneTransitionLayoutState.observableTransitionState] instead.
+     */
+    var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
+        internal set
+}
+
+sealed interface TransitionState {
+    /**
+     * The current effective scene. If a new transition was triggered, it would start from this
+     * scene.
+     *
+     * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
+     * gesture starts, but then if the user flings their finger and commits the transition to scene
+     * B, then [currentScene] becomes scene B even if the transition is not finished yet and is
+     * still animating to settle to scene B.
+     */
+    val currentScene: SceneKey
+
+    /** No transition/animation is currently running. */
+    data class Idle(override val currentScene: SceneKey) : TransitionState
+
+    /**
+     * There is a transition animating between two scenes.
+     *
+     * Important note: [fromScene] and [toScene] might be the same, in which case this [Transition]
+     * should be treated the same as [Idle]. This is designed on purpose so that a [Transition] can
+     * be started without knowing in advance where it is transitioning to, making the logic of
+     * [swipeToScene] easier to reason about.
+     */
+    interface Transition : TransitionState {
+        /** The scene this transition is starting from. */
+        val fromScene: SceneKey
+
+        /** The scene this transition is going to. */
+        val toScene: SceneKey
+
+        /**
+         * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
+         * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
+         * when flinging quickly during a swipe gesture.
+         */
+        val progress: Float
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
new file mode 100644
index 0000000..9752f53
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.snap
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.ModifierTransformation
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.Translate
+import com.android.compose.ui.util.fastForEach
+import com.android.compose.ui.util.fastMap
+
+/** The transitions configuration of a [SceneTransitionLayout]. */
+class SceneTransitions(
+    val transitionSpecs: List<TransitionSpec>,
+) {
+    private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
+
+    internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+        return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
+    }
+
+    private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+        val spec = transition(from, to) { it.from == from && it.to == to }
+        if (spec != null) {
+            return spec
+        }
+
+        val reversed = transition(from, to) { it.from == to && it.to == from }
+        if (reversed != null) {
+            return reversed.reverse()
+        }
+
+        val relaxedSpec =
+            transition(from, to) {
+                (it.from == from && it.to == null) || (it.to == to && it.from == null)
+            }
+        if (relaxedSpec != null) {
+            return relaxedSpec
+        }
+
+        return transition(from, to) {
+                (it.from == to && it.to == null) || (it.to == from && it.from == null)
+            }
+            ?.reverse()
+            ?: defaultTransition(from, to)
+    }
+
+    private fun transition(
+        from: SceneKey,
+        to: SceneKey,
+        filter: (TransitionSpec) -> Boolean,
+    ): TransitionSpec? {
+        var match: TransitionSpec? = null
+        transitionSpecs.fastForEach { spec ->
+            if (filter(spec)) {
+                if (match != null) {
+                    error("Found multiple transition specs for transition $from => $to")
+                }
+                match = spec
+            }
+        }
+        return match
+    }
+
+    private fun defaultTransition(from: SceneKey, to: SceneKey) =
+        TransitionSpec(from, to, emptyList(), snap())
+}
+
+/** The definition of a transition between [from] and [to]. */
+data class TransitionSpec(
+    val from: SceneKey?,
+    val to: SceneKey?,
+    val transformations: List<Transformation>,
+    val spec: AnimationSpec<Float>,
+) {
+    private val cache = mutableMapOf<ElementKey, ElementTransformations>()
+
+    internal fun reverse(): TransitionSpec {
+        return copy(
+            from = to,
+            to = from,
+            transformations = transformations.fastMap { it.reverse() },
+        )
+    }
+
+    internal fun transformations(element: ElementKey): ElementTransformations {
+        return cache.getOrPut(element) { computeTransformations(element) }
+    }
+
+    /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
+    private fun computeTransformations(element: ElementKey): ElementTransformations {
+        val modifier = mutableListOf<ModifierTransformation>()
+        var offset: PropertyTransformation<Offset>? = null
+        var size: PropertyTransformation<IntSize>? = null
+        var alpha: PropertyTransformation<Float>? = null
+
+        fun <T> onPropertyTransformation(
+            root: PropertyTransformation<T>,
+            current: PropertyTransformation<T> = root,
+        ) {
+            when (current) {
+                is Translate,
+                is EdgeTranslate,
+                is AnchoredTranslate -> {
+                    throwIfNotNull(offset, element, property = "offset")
+                    offset = root as PropertyTransformation<Offset>
+                }
+                is ScaleSize,
+                is AnchoredSize -> {
+                    throwIfNotNull(size, element, property = "size")
+                    size = root as PropertyTransformation<IntSize>
+                }
+                is Fade -> {
+                    throwIfNotNull(alpha, element, property = "alpha")
+                    alpha = root as PropertyTransformation<Float>
+                }
+                is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
+            }
+        }
+
+        transformations.fastForEach { transformation ->
+            if (!transformation.matcher.matches(element)) {
+                return@fastForEach
+            }
+
+            when (transformation) {
+                is ModifierTransformation -> modifier.add(transformation)
+                is PropertyTransformation<*> -> onPropertyTransformation(transformation)
+            }
+        }
+
+        return ElementTransformations(modifier, offset, size, alpha)
+    }
+
+    private fun throwIfNotNull(
+        previous: PropertyTransformation<*>?,
+        element: ElementKey,
+        property: String,
+    ) {
+        if (previous != null) {
+            error("$element has multiple transformations for its $property property")
+        }
+    }
+}
+
+/** The transformations of an element during a transition. */
+internal class ElementTransformations(
+    val modifier: List<ModifierTransformation>,
+    val offset: PropertyTransformation<Offset>?,
+    val size: PropertyTransformation<IntSize>?,
+    val alpha: PropertyTransformation<Float>?,
+)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
new file mode 100644
index 0000000..d9a45cd
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
+ */
+@Composable
+internal fun Modifier.swipeToScene(
+    layoutImpl: SceneTransitionLayoutImpl,
+    orientation: Orientation,
+): Modifier {
+    val state = layoutImpl.state.transitionState
+    val currentScene = layoutImpl.scene(state.currentScene)
+    val transition = remember {
+        // Note that the currentScene here does not matter, it's only used for initializing the
+        // transition and will be replaced when a drag event starts.
+        SwipeTransition(initialScene = currentScene)
+    }
+
+    val enabled = state == transition || currentScene.shouldEnableSwipes(orientation)
+
+    // Immediately start the drag if this our [transition] is currently animating to a scene (i.e.
+    // the user released their input pointer after swiping in this orientation) and the user can't
+    // swipe in the other direction.
+    val startDragImmediately =
+        state == transition &&
+            transition.isAnimatingOffset &&
+            !currentScene.shouldEnableSwipes(orientation.opposite())
+
+    // The velocity threshold at which the intent of the user is to swipe up or down. It is the same
+    // as SwipeableV2Defaults.VelocityThreshold.
+    val velocityThreshold = with(LocalDensity.current) { 125.dp.toPx() }
+
+    // The positional threshold at which the intent of the user is to swipe to the next scene. It is
+    // the same as SwipeableV2Defaults.PositionalThreshold.
+    val positionalThreshold = with(LocalDensity.current) { 56.dp.toPx() }
+
+    return draggable(
+        orientation = orientation,
+        enabled = enabled,
+        startDragImmediately = startDragImmediately,
+        onDragStarted = { onDragStarted(layoutImpl, transition, orientation) },
+        state =
+            rememberDraggableState { delta -> onDrag(layoutImpl, transition, orientation, delta) },
+        onDragStopped = { velocity ->
+            onDragStopped(
+                layoutImpl,
+                transition,
+                velocity,
+                velocityThreshold,
+                positionalThreshold,
+            )
+        },
+    )
+}
+
+private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+    var _currentScene by mutableStateOf(initialScene)
+    override val currentScene: SceneKey
+        get() = _currentScene.key
+
+    var _fromScene by mutableStateOf(initialScene)
+    override val fromScene: SceneKey
+        get() = _fromScene.key
+
+    var _toScene by mutableStateOf(initialScene)
+    override val toScene: SceneKey
+        get() = _toScene.key
+
+    override val progress: Float
+        get() {
+            val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+            if (distance == 0f) {
+                // This can happen only if fromScene == toScene.
+                error(
+                    "Transition.progress should be called only when Transition.fromScene != " +
+                        "Transition.toScene"
+                )
+            }
+            return offset / distance
+        }
+
+    /** The current offset caused by the drag gesture. */
+    var dragOffset by mutableFloatStateOf(0f)
+
+    /**
+     * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+     */
+    var isAnimatingOffset by mutableStateOf(false)
+
+    /** The animatable used to animate the offset once the user lifted its finger. */
+    val offsetAnimatable = Animatable(0f, visibilityThreshold = OffsetVisibilityThreshold)
+
+    /**
+     * The job currently animating [offsetAnimatable], if it is animating. Note that setting this to
+     * a new job will automatically cancel the previous one.
+     */
+    var offsetAnimationJob: Job? = null
+        set(value) {
+            field?.cancel()
+            field = value
+        }
+
+    /** The absolute distance between [fromScene] and [toScene]. */
+    var absoluteDistance = 0f
+
+    /**
+     * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+     * or to the left of [toScene].
+     */
+    var _distance by mutableFloatStateOf(0f)
+    val distance: Float
+        get() = _distance
+}
+
+/** The destination scene when swiping up or left from [this@upOrLeft]. */
+private fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
+    return when (orientation) {
+        Orientation.Vertical -> userActions[Swipe.Up]
+        Orientation.Horizontal -> userActions[Swipe.Left]
+    }
+}
+
+/** The destination scene when swiping down or right from [this@downOrRight]. */
+private fun Scene.downOrRight(orientation: Orientation): SceneKey? {
+    return when (orientation) {
+        Orientation.Vertical -> userActions[Swipe.Down]
+        Orientation.Horizontal -> userActions[Swipe.Right]
+    }
+}
+
+/** Whether swipe should be enabled in the given [orientation]. */
+private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
+    return upOrLeft(orientation) != null || downOrRight(orientation) != null
+}
+
+private fun Orientation.opposite(): Orientation {
+    return when (this) {
+        Orientation.Vertical -> Orientation.Horizontal
+        Orientation.Horizontal -> Orientation.Vertical
+    }
+}
+
+private fun onDragStarted(
+    layoutImpl: SceneTransitionLayoutImpl,
+    transition: SwipeTransition,
+    orientation: Orientation,
+) {
+    if (layoutImpl.state.transitionState == transition) {
+        // This [transition] was already driving the animation: simply take over it.
+        if (transition.isAnimatingOffset) {
+            // Stop animating and start from where the current offset. Setting the animation job to
+            // `null` will effectively cancel the animation.
+            transition.isAnimatingOffset = false
+            transition.offsetAnimationJob = null
+            transition.dragOffset = transition.offsetAnimatable.value
+        }
+
+        return
+    }
+
+    // TODO(b/290184746): Better handle interruptions here if state != idle.
+
+    val fromScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
+
+    transition._currentScene = fromScene
+    transition._fromScene = fromScene
+
+    // We don't know where we are transitioning to yet given that the drag just started, so set it
+    // to fromScene, which will effectively be treated the same as Idle(fromScene).
+    transition._toScene = fromScene
+
+    transition.dragOffset = 0f
+    transition.isAnimatingOffset = false
+    transition.offsetAnimationJob = null
+
+    // Use the layout size in the swipe orientation for swipe distance.
+    // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
+    // will also have to make sure that we correctly handle overscroll.
+    transition.absoluteDistance =
+        when (orientation) {
+            Orientation.Horizontal -> layoutImpl.size.width
+            Orientation.Vertical -> layoutImpl.size.height
+        }.toFloat()
+
+    if (transition.absoluteDistance > 0f) {
+        layoutImpl.state.transitionState = transition
+    }
+}
+
+private fun onDrag(
+    layoutImpl: SceneTransitionLayoutImpl,
+    transition: SwipeTransition,
+    orientation: Orientation,
+    delta: Float,
+) {
+    transition.dragOffset += delta
+
+    // First check transition.fromScene should be changed for the case where the user quickly swiped
+    // twice in a row to accelerate the transition and go from A => B then B => C really fast.
+    maybeHandleAcceleratedSwipe(transition, orientation)
+
+    val fromScene = transition._fromScene
+    val upOrLeft = fromScene.upOrLeft(orientation)
+    val downOrRight = fromScene.downOrRight(orientation)
+    val offset = transition.dragOffset
+
+    // Compute the target scene depending on the current offset.
+    val targetSceneKey: SceneKey
+    val signedDistance: Float
+    when {
+        offset < 0f && upOrLeft != null -> {
+            targetSceneKey = upOrLeft
+            signedDistance = -transition.absoluteDistance
+        }
+        offset > 0f && downOrRight != null -> {
+            targetSceneKey = downOrRight
+            signedDistance = transition.absoluteDistance
+        }
+        else -> {
+            targetSceneKey = fromScene.key
+            signedDistance = 0f
+        }
+    }
+
+    if (transition._toScene.key != targetSceneKey) {
+        transition._toScene = layoutImpl.scenes.getValue(targetSceneKey)
+    }
+
+    if (transition._distance != signedDistance) {
+        transition._distance = signedDistance
+    }
+}
+
+/**
+ * Change fromScene in the case where the user quickly swiped multiple times in the same direction
+ * to accelerate the transition from A => B then B => C.
+ */
+private fun maybeHandleAcceleratedSwipe(
+    transition: SwipeTransition,
+    orientation: Orientation,
+) {
+    val toScene = transition._toScene
+    val fromScene = transition._fromScene
+
+    // If the swipe was not committed, don't do anything.
+    if (fromScene == toScene || transition._currentScene != toScene) {
+        return
+    }
+
+    // If the offset is past the distance then let's change fromScene so that the user can swipe to
+    // the next screen or go back to the previous one.
+    val offset = transition.dragOffset
+    val absoluteDistance = transition.absoluteDistance
+    if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
+        transition.dragOffset += absoluteDistance
+        transition._fromScene = toScene
+    } else if (offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key) {
+        transition.dragOffset -= absoluteDistance
+        transition._fromScene = toScene
+    }
+
+    // Important note: toScene and distance will be updated right after this function is called,
+    // using fromScene and dragOffset.
+}
+
+private fun CoroutineScope.onDragStopped(
+    layoutImpl: SceneTransitionLayoutImpl,
+    transition: SwipeTransition,
+    velocity: Float,
+    velocityThreshold: Float,
+    positionalThreshold: Float,
+) {
+    // The state was changed since the drag started; don't do anything.
+    if (layoutImpl.state.transitionState != transition) {
+        return
+    }
+
+    // We were not animating.
+    if (transition._fromScene == transition._toScene) {
+        layoutImpl.state.transitionState = TransitionState.Idle(transition._fromScene.key)
+        return
+    }
+
+    // Compute the destination scene (and therefore offset) to settle in.
+    val targetScene: Scene
+    val targetOffset: Float
+    val offset = transition.dragOffset
+    val distance = transition.distance
+    if (
+        shouldCommitSwipe(
+            offset,
+            distance,
+            velocity,
+            velocityThreshold,
+            positionalThreshold,
+            wasCommitted = transition._currentScene == transition._toScene,
+        )
+    ) {
+        targetOffset = distance
+        targetScene = transition._toScene
+    } else {
+        targetOffset = 0f
+        targetScene = transition._fromScene
+    }
+
+    // If the effective current scene changed, it should be reflected right now in the current scene
+    // state, even before the settle animation is ongoing. That way all the swipeables and back
+    // handlers will be refreshed and the user can for instance quickly swipe vertically from A => B
+    // then horizontally from B => C, or swipe from A => B then immediately go back B => A.
+    if (targetScene != transition._currentScene) {
+        transition._currentScene = targetScene
+        layoutImpl.onChangeScene(targetScene.key)
+    }
+
+    // Animate the offset.
+    transition.offsetAnimationJob = launch {
+        transition.offsetAnimatable.snapTo(offset)
+        transition.isAnimatingOffset = true
+
+        transition.offsetAnimatable.animateTo(
+            targetOffset,
+            // TODO(b/290184746): Make this spring spec configurable.
+            spring(
+                stiffness = Spring.StiffnessMediumLow,
+                visibilityThreshold = OffsetVisibilityThreshold
+            ),
+            initialVelocity = velocity,
+        )
+
+        // Now that the animation is done, the state should be idle. Note that if the state was
+        // changed since this animation started, some external code changed it and we shouldn't do
+        // anything here. Note also that this job will be cancelled in the case where the user
+        // intercepts this swipe.
+        if (layoutImpl.state.transitionState == transition) {
+            layoutImpl.state.transitionState = TransitionState.Idle(targetScene.key)
+        }
+
+        transition.offsetAnimationJob = null
+    }
+}
+
+/**
+ * Whether the swipe to the target scene should be committed or not. This is inspired by
+ * SwipeableV2.computeTarget().
+ */
+private fun shouldCommitSwipe(
+    offset: Float,
+    distance: Float,
+    velocity: Float,
+    velocityThreshold: Float,
+    positionalThreshold: Float,
+    wasCommitted: Boolean,
+): Boolean {
+    fun isCloserToTarget(): Boolean {
+        return (offset - distance).absoluteValue < offset.absoluteValue
+    }
+
+    // Swiping up or left.
+    if (distance < 0f) {
+        return if (offset > 0f || velocity >= velocityThreshold) {
+            false
+        } else {
+            velocity <= -velocityThreshold ||
+                (offset <= -positionalThreshold && !wasCommitted) ||
+                isCloserToTarget()
+        }
+    }
+
+    // Swiping down or right.
+    return if (offset < 0f || velocity <= -velocityThreshold) {
+        false
+    } else {
+        velocity >= velocityThreshold ||
+            (offset >= positionalThreshold && !wasCommitted) ||
+            isCloserToTarget()
+    }
+}
+
+/**
+ * The number of pixels below which there won't be a visible difference in the transition and from
+ * which the animation can stop.
+ */
+private const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
new file mode 100644
index 0000000..fb12b90
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
+fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
+    return transitionsImpl(builder)
+}
+
+@DslMarker annotation class TransitionDsl
+
+@TransitionDsl
+interface SceneTransitionsBuilder {
+    /**
+     * Define the default animation to be played when transitioning [to] the specified scene, from
+     * any scene. For the animation specification to apply only when transitioning between two
+     * specific scenes, use [from] instead.
+     *
+     * @see from
+     */
+    fun to(
+        to: SceneKey,
+        builder: TransitionBuilder.() -> Unit = {},
+    ): TransitionSpec
+
+    /**
+     * Define the animation to be played when transitioning [from] the specified scene. For the
+     * animation specification to apply only when transitioning between two specific scenes, pass
+     * the destination scene via the [to] argument.
+     *
+     * When looking up which transition should be used when animating from scene A to scene B, we
+     * pick the single transition matching one of these predicates (in order of importance):
+     * 1. from == A && to == B
+     * 2. to == A && from == B, which is then treated in reverse.
+     * 3. (from == A && to == null) || (from == null && to == B)
+     * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse.
+     */
+    fun from(
+        from: SceneKey,
+        to: SceneKey? = null,
+        builder: TransitionBuilder.() -> Unit = {},
+    ): TransitionSpec
+}
+
+@TransitionDsl
+interface TransitionBuilder : PropertyTransformationBuilder {
+    /**
+     * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
+     * performing programmatic (not input pointer tracking) animations.
+     */
+    var spec: AnimationSpec<Float>
+
+    /**
+     * Define a progress-based range for the transformations inside [builder].
+     *
+     * For instance, the following will fade `Foo` during the first half of the transition then it
+     * will translate it by 100.dp during the second half.
+     *
+     * ```
+     * fractionRange(end = 0.5f) { fade(Foo) }
+     * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) }
+     * ```
+     *
+     * @param start the start of the range, in the [0; 1] range.
+     * @param end the end of the range, in the [0; 1] range.
+     */
+    fun fractionRange(
+        start: Float? = null,
+        end: Float? = null,
+        builder: PropertyTransformationBuilder.() -> Unit,
+    )
+
+    /**
+     * Define a timestamp-based range for the transformations inside [builder].
+     *
+     * For instance, the following will fade `Foo` during the first half of the transition then it
+     * will translate it by 100.dp during the second half.
+     *
+     * ```
+     * spec = tween(500)
+     * timestampRange(end = 250) { fade(Foo) }
+     * timestampRange(start = 250) { translate(Foo, x = 100.dp) }
+     * ```
+     *
+     * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if
+     * you call [timestampRange], otherwise this will throw. The spec duration will be used to
+     * transform this range into a [fractionRange].
+     *
+     * @param startMillis the start of the range, in the [0; spec.duration] range.
+     * @param endMillis the end of the range, in the [0; spec.duration] range.
+     */
+    fun timestampRange(
+        startMillis: Int? = null,
+        endMillis: Int? = null,
+        builder: PropertyTransformationBuilder.() -> Unit,
+    )
+
+    /**
+     * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and
+     * using the given [shape].
+     *
+     * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
+     * This can be used to make content drawn below an opaque element visible. For example, if we
+     * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
+     * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
+     * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
+     * the result.
+     */
+    fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
+}
+
+@TransitionDsl
+interface PropertyTransformationBuilder {
+    /**
+     * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the
+     * element is entering or leaving the scene, respectively.
+     */
+    fun fade(matcher: ElementMatcher)
+
+    /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */
+    fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp)
+
+    /**
+     * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout]
+     * animating it.
+     *
+     * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of
+     * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its
+     * content). If it is `false`, then the element will start aligned with the edge of the layout
+     * (i.e. it will be completely visible at progress = 0f).
+     */
+    fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true)
+
+    /**
+     * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated
+     * during this transition.
+     *
+     * Note: This currently only works if [anchor] is a shared element of this transition.
+     *
+     * TODO(b/290184746): Also support anchors that are not shared but translated because of other
+     *   transformations, like an edge translation.
+     */
+    fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey)
+
+    /**
+     * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling
+     * is done during layout, so it will potentially impact the size and position of other elements.
+     *
+     * TODO(b/290184746): Also provide a scaleDrawing() to scale an element at drawing time.
+     */
+    fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f)
+
+    /**
+     * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as [anchor]
+     * .
+     *
+     * Note: This currently only works if [anchor] is a shared element of this transition.
+     */
+    fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey)
+}
+
+/** An interface to match one or more elements. */
+interface ElementMatcher {
+    /** Whether the element with key [key] matches this matcher. */
+    fun matches(key: ElementKey): Boolean
+}
+
+/** The edge of a [SceneTransitionLayout]. */
+enum class Edge {
+    Left,
+    Right,
+    Top,
+    Bottom,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
new file mode 100644
index 0000000..afd49b4
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DurationBasedAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.PunchHole
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationRange
+import com.android.compose.animation.scene.transformation.Translate
+
+internal fun transitionsImpl(
+    builder: SceneTransitionsBuilder.() -> Unit,
+): SceneTransitions {
+    val impl = SceneTransitionsBuilderImpl().apply(builder)
+    return SceneTransitions(impl.transitionSpecs)
+}
+
+private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
+    val transitionSpecs = mutableListOf<TransitionSpec>()
+
+    override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
+        return transition(from = null, to = to, builder)
+    }
+
+    override fun from(
+        from: SceneKey,
+        to: SceneKey?,
+        builder: TransitionBuilder.() -> Unit
+    ): TransitionSpec {
+        return transition(from = from, to = to, builder)
+    }
+
+    private fun transition(
+        from: SceneKey?,
+        to: SceneKey?,
+        builder: TransitionBuilder.() -> Unit,
+    ): TransitionSpec {
+        val impl = TransitionBuilderImpl().apply(builder)
+        val spec =
+            TransitionSpec(
+                from,
+                to,
+                impl.transformations,
+                impl.spec,
+            )
+        transitionSpecs.add(spec)
+        return spec
+    }
+}
+
+private class TransitionBuilderImpl : TransitionBuilder {
+    val transformations = mutableListOf<Transformation>()
+    override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
+
+    private var range: TransformationRange? = null
+    private val durationMillis: Int by lazy {
+        val spec = spec
+        if (spec !is DurationBasedAnimationSpec) {
+            error("timestampRange {} can only be used with a DurationBasedAnimationSpec")
+        }
+
+        spec.vectorize(Float.VectorConverter).durationMillis
+    }
+
+    override fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape) {
+        transformations.add(PunchHole(matcher, bounds, shape))
+    }
+
+    override fun fractionRange(
+        start: Float?,
+        end: Float?,
+        builder: PropertyTransformationBuilder.() -> Unit
+    ) {
+        range = TransformationRange(start, end)
+        builder()
+        range = null
+    }
+
+    override fun timestampRange(
+        startMillis: Int?,
+        endMillis: Int?,
+        builder: PropertyTransformationBuilder.() -> Unit
+    ) {
+        if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) {
+            error("invalid start value: startMillis=$startMillis durationMillis=$durationMillis")
+        }
+
+        if (endMillis != null && (endMillis < 0 || endMillis > durationMillis)) {
+            error("invalid end value: endMillis=$startMillis durationMillis=$durationMillis")
+        }
+
+        val start = startMillis?.let { it.toFloat() / durationMillis }
+        val end = endMillis?.let { it.toFloat() / durationMillis }
+        fractionRange(start, end, builder)
+    }
+
+    private fun transformation(transformation: PropertyTransformation<*>) {
+        if (range != null) {
+            transformations.add(RangedPropertyTransformation(transformation, range!!))
+        } else {
+            transformations.add(transformation)
+        }
+    }
+
+    override fun fade(matcher: ElementMatcher) {
+        transformation(Fade(matcher))
+    }
+
+    override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
+        transformation(Translate(matcher, x, y))
+    }
+
+    override fun translate(
+        matcher: ElementMatcher,
+        edge: Edge,
+        startsOutsideLayoutBounds: Boolean
+    ) {
+        transformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
+    }
+
+    override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
+        transformation(AnchoredTranslate(matcher, anchor))
+    }
+
+    override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
+        transformation(ScaleSize(matcher, width, height))
+    }
+
+    override fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) {
+        transformation(AnchoredSize(matcher, anchor))
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
new file mode 100644
index 0000000..d4ed697
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the size of an element to the size of another element. */
+internal class AnchoredSize(
+    override val matcher: ElementMatcher,
+    private val anchor: ElementKey,
+) : PropertyTransformation<IntSize> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: IntSize,
+    ): IntSize {
+        fun anchorSizeIn(scene: SceneKey): IntSize {
+            val size = layoutImpl.elements[anchor]?.sceneValues?.get(scene)?.size
+            return if (size != null && size != Element.SizeUnspecified) {
+                size
+            } else {
+                value
+            }
+        }
+
+        // This simple implementation assumes that the size of [element] is the same as the size of
+        // the [anchor] in [scene], so simply transform to the size of the anchor in the other
+        // scene.
+        return if (scene.key == transition.fromScene) {
+            anchorSizeIn(transition.toScene)
+        } else {
+            anchorSizeIn(transition.fromScene)
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
new file mode 100644
index 0000000..8a5bd74
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the translation of an element to another element. */
+internal class AnchoredTranslate(
+    override val matcher: ElementMatcher,
+    private val anchor: ElementKey,
+) : PropertyTransformation<Offset> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: Offset,
+    ): Offset {
+        val anchor = layoutImpl.elements[anchor] ?: return value
+        fun anchorOffsetIn(scene: SceneKey): Offset? {
+            return anchor.sceneValues[scene]?.offset?.takeIf { it.isSpecified }
+        }
+
+        // [element] will move the same amount as [anchor] does.
+        // TODO(b/290184746): Also support anchors that are not shared but translated because of
+        // other transformations, like an edge translation.
+        val anchorFromOffset = anchorOffsetIn(transition.fromScene) ?: return value
+        val anchorToOffset = anchorOffsetIn(transition.toScene) ?: return value
+        val offset = anchorToOffset - anchorFromOffset
+
+        return if (scene.key == transition.toScene) {
+            Offset(
+                value.x - offset.x,
+                value.y - offset.y,
+            )
+        } else {
+            Offset(
+                value.x + offset.x,
+                value.y + offset.y,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
new file mode 100644
index 0000000..5cdce94
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element from an edge of the layout. */
+internal class EdgeTranslate(
+    override val matcher: ElementMatcher,
+    private val edge: Edge,
+    private val startsOutsideLayoutBounds: Boolean = true,
+) : PropertyTransformation<Offset> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: Offset
+    ): Offset {
+        val sceneSize = scene.size
+        val elementSize = sceneValues.size
+        if (elementSize == Element.SizeUnspecified) {
+            return value
+        }
+
+        return when (edge) {
+            Edge.Top ->
+                if (startsOutsideLayoutBounds) {
+                    Offset(value.x, -elementSize.height.toFloat())
+                } else {
+                    Offset(value.x, 0f)
+                }
+            Edge.Left ->
+                if (startsOutsideLayoutBounds) {
+                    Offset(-elementSize.width.toFloat(), value.y)
+                } else {
+                    Offset(0f, value.y)
+                }
+            Edge.Bottom ->
+                if (startsOutsideLayoutBounds) {
+                    Offset(value.x, sceneSize.height.toFloat())
+                } else {
+                    Offset(value.x, (sceneSize.height - elementSize.height).toFloat())
+                }
+            Edge.Right ->
+                if (startsOutsideLayoutBounds) {
+                    Offset(sceneSize.width.toFloat(), value.y)
+                } else {
+                    Offset((sceneSize.width - elementSize.width).toFloat(), value.y)
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
new file mode 100644
index 0000000..0a5ac54
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Fade an element in or out. */
+internal class Fade(
+    override val matcher: ElementMatcher,
+) : PropertyTransformation<Float> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: Float
+    ): Float {
+        // Return the alpha value of [element] either when it starts fading in or when it finished
+        // fading out, which is `0` in both cases.
+        return 0f
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
new file mode 100644
index 0000000..31e7d7c
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.unit.toSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+
+/** Punch a hole in an element using the bounds of another element and a given [shape]. */
+internal class PunchHole(
+    override val matcher: ElementMatcher,
+    private val bounds: ElementKey,
+    private val shape: Shape,
+) : ModifierTransformation {
+    override fun Modifier.transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+    ): Modifier {
+        return drawWithContent {
+            val bounds = layoutImpl.elements[bounds]
+            if (
+                bounds == null ||
+                    bounds.lastSize == Element.SizeUnspecified ||
+                    bounds.lastOffset == Offset.Unspecified
+            ) {
+                drawContent()
+                return@drawWithContent
+            }
+
+            drawIntoCanvas { canvas ->
+                canvas.withSaveLayer(size.toRect(), Paint()) {
+                    drawContent()
+
+                    val offset = bounds.lastOffset - element.lastOffset
+                    translate(offset.x, offset.y) { drawHole(bounds) }
+                }
+            }
+        }
+    }
+
+    private fun DrawScope.drawHole(bounds: Element) {
+        if (shape == RectangleShape) {
+            drawRect(Color.Black, blendMode = BlendMode.DstOut)
+            return
+        }
+
+        // TODO(b/290184746): Cache outline if the size of bounds does not change.
+        drawOutline(
+            shape.createOutline(
+                bounds.lastSize.toSize(),
+                layoutDirection,
+                this,
+            ),
+            Color.Black,
+            blendMode = BlendMode.DstOut,
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
new file mode 100644
index 0000000..ce754dc
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+import kotlin.math.roundToInt
+
+/**
+ * Scales the size of an element. Note that this makes the element resize every frame and will
+ * therefore impact the layout of other elements.
+ */
+internal class ScaleSize(
+    override val matcher: ElementMatcher,
+    private val width: Float = 1f,
+    private val height: Float = 1f,
+) : PropertyTransformation<IntSize> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: IntSize,
+    ): IntSize {
+        return IntSize(
+            width = (value.width * width).roundToInt(),
+            height = (value.height * height).roundToInt(),
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
new file mode 100644
index 0000000..ce6749d
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** A transformation applied to one or more elements during a transition. */
+sealed interface Transformation {
+    /**
+     * The matcher that should match the element(s) to which this transformation should be applied.
+     */
+    val matcher: ElementMatcher
+
+    /*
+     * Reverse this transformation. This is called when we use Transition(from = A, to = B) when
+     * animating from B to A and there is no Transition(from = B, to = A) defined.
+     */
+    fun reverse(): Transformation = this
+}
+
+/** A transformation that is applied on the element during the whole transition. */
+internal interface ModifierTransformation : Transformation {
+    /** Apply the transformation to [element]. */
+    // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+    // to these internal classes.
+    fun Modifier.transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+    ): Modifier
+}
+
+/** A transformation that changes the value of an element property, like its size or offset. */
+internal sealed interface PropertyTransformation<T> : Transformation {
+    /**
+     * The range during which the transformation is applied. If it is `null`, then the
+     * transformation will be applied throughout the whole scene transition.
+     */
+    val range: TransformationRange?
+        get() = null
+
+    /**
+     * Transform [value], i.e. the value of the transformed property without this transformation.
+     */
+    // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+    // to these internal classes.
+    fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: T,
+    ): T
+}
+
+/**
+ * A [PropertyTransformation] associated to a range. This is a helper class so that normal
+ * implementations of [PropertyTransformation] don't have to take care of reversing their range when
+ * they are reversed.
+ */
+internal class RangedPropertyTransformation<T>(
+    val delegate: PropertyTransformation<T>,
+    override val range: TransformationRange,
+) : PropertyTransformation<T> by delegate {
+    override fun reverse(): Transformation {
+        return RangedPropertyTransformation(
+            delegate.reverse() as PropertyTransformation<T>,
+            range.reverse()
+        )
+    }
+}
+
+/** The progress-based range of a [PropertyTransformation]. */
+data class TransformationRange
+private constructor(
+    val start: Float,
+    val end: Float,
+) {
+    constructor(
+        start: Float? = null,
+        end: Float? = null
+    ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified)
+
+    init {
+        require(!start.isSpecified() || (start in 0f..1f))
+        require(!end.isSpecified() || (end in 0f..1f))
+        require(!start.isSpecified() || !end.isSpecified() || start <= end)
+    }
+
+    /** Reverse this range. */
+    fun reverse() = TransformationRange(start = reverseBound(end), end = reverseBound(start))
+
+    /** Get the progress of this range given the global [transitionProgress]. */
+    fun progress(transitionProgress: Float): Float {
+        return when {
+            start.isSpecified() && end.isSpecified() ->
+                ((transitionProgress - start) / (end - start)).coerceIn(0f, 1f)
+            !start.isSpecified() && !end.isSpecified() -> transitionProgress
+            end.isSpecified() -> (transitionProgress / end).coerceAtMost(1f)
+            else -> ((transitionProgress - start) / (1f - start)).coerceAtLeast(0f)
+        }
+    }
+
+    private fun Float.isSpecified() = this != BoundUnspecified
+
+    private fun reverseBound(bound: Float): Float {
+        return if (bound.isSpecified()) {
+            1f - bound
+        } else {
+            BoundUnspecified
+        }
+    }
+
+    companion object {
+        private const val BoundUnspecified = Float.MIN_VALUE
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
new file mode 100644
index 0000000..8abca61
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element by a fixed amount of density-independent pixels. */
+internal class Translate(
+    override val matcher: ElementMatcher,
+    private val x: Dp = 0.dp,
+    private val y: Dp = 0.dp,
+) : PropertyTransformation<Offset> {
+    override fun transform(
+        layoutImpl: SceneTransitionLayoutImpl,
+        scene: Scene,
+        element: Element,
+        sceneValues: Element.SceneValues,
+        transition: TransitionState.Transition,
+        value: Offset,
+    ): Offset {
+        return with(layoutImpl.density) {
+            Offset(
+                value.x + x.toPx(),
+                value.y + y.toPx(),
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
index 5224c51..27f0948 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
 import kotlin.math.ceil
 import kotlin.math.max
@@ -126,18 +125,20 @@
             ((columns - 1) * horizontalSpacing.toPx()).roundToInt()
         val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
         val childConstraints =
-            Constraints().apply {
-                if (constraints.maxWidth != Constraints.Infinity) {
-                    constrainWidth(
+            Constraints(
+                maxWidth =
+                    if (constraints.maxWidth != Constraints.Infinity) {
                         (constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
-                    )
-                }
-                if (constraints.maxHeight != Constraints.Infinity) {
-                    constrainWidth(
+                    } else {
+                        Constraints.Infinity
+                    },
+                maxHeight =
+                    if (constraints.maxHeight != Constraints.Infinity) {
                         (constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
-                    )
-                }
-            }
+                    } else {
+                        Constraints.Infinity
+                    }
+            )
 
         val placeables = buildList {
             for (cellIndex in measurables.indices) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
similarity index 96%
rename from packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt
rename to packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
index 83071d7..135a6e4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.compose.modifiers
+package com.android.compose.modifiers
 
 import androidx.compose.ui.Modifier
 
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
deleted file mode 100644
index 946e779..0000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
+++ /dev/null
@@ -1,849 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.compose.swipeable
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.foundation.gestures.DraggableState
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec
-import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor
-import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold
-import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig
-import com.android.compose.ui.util.lerp
-import kotlin.math.PI
-import kotlin.math.abs
-import kotlin.math.sign
-import kotlin.math.sin
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.launch
-
-/**
- * State of the [swipeable] modifier.
- *
- * This contains necessary information about any ongoing swipe or animation and provides methods to
- * change the state either immediately or by starting an animation. To create and remember a
- * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- *
- * TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3.
- */
-@Stable
-open class SwipeableState<T>(
-    initialValue: T,
-    internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
-    internal val confirmStateChange: (newValue: T) -> Boolean = { true }
-) {
-    /**
-     * The current value of the state.
-     *
-     * If no swipe or animation is in progress, this corresponds to the anchor at which the
-     * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
-     * the last anchor at which the [swipeable] was settled before the swipe or animation started.
-     */
-    var currentValue: T by mutableStateOf(initialValue)
-        private set
-
-    /** Whether the state is currently animating. */
-    var isAnimationRunning: Boolean by mutableStateOf(false)
-        private set
-
-    /**
-     * The current position (in pixels) of the [swipeable].
-     *
-     * You should use this state to offset your content accordingly. The recommended way is to use
-     * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
-     */
-    val offset: State<Float>
-        get() = offsetState
-
-    /** The amount by which the [swipeable] has been swiped past its bounds. */
-    val overflow: State<Float>
-        get() = overflowState
-
-    // Use `Float.NaN` as a placeholder while the state is uninitialised.
-    private val offsetState = mutableStateOf(0f)
-    private val overflowState = mutableStateOf(0f)
-
-    // the source of truth for the "real"(non ui) position
-    // basically position in bounds + overflow
-    private val absoluteOffset = mutableStateOf(0f)
-
-    // current animation target, if animating, otherwise null
-    private val animationTarget = mutableStateOf<Float?>(null)
-
-    internal var anchors by mutableStateOf(emptyMap<Float, T>())
-
-    private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
-        snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
-
-    internal var minBound = Float.NEGATIVE_INFINITY
-    internal var maxBound = Float.POSITIVE_INFINITY
-
-    internal fun ensureInit(newAnchors: Map<Float, T>) {
-        if (anchors.isEmpty()) {
-            // need to do initial synchronization synchronously :(
-            val initialOffset = newAnchors.getOffset(currentValue)
-            requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
-            offsetState.value = initialOffset
-            absoluteOffset.value = initialOffset
-        }
-    }
-
-    internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
-        if (oldAnchors.isEmpty()) {
-            // If this is the first time that we receive anchors, then we need to initialise
-            // the state so we snap to the offset associated to the initial value.
-            minBound = newAnchors.keys.minOrNull()!!
-            maxBound = newAnchors.keys.maxOrNull()!!
-            val initialOffset = newAnchors.getOffset(currentValue)
-            requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
-            snapInternalToOffset(initialOffset)
-        } else if (newAnchors != oldAnchors) {
-            // If we have received new anchors, then the offset of the current value might
-            // have changed, so we need to animate to the new offset. If the current value
-            // has been removed from the anchors then we animate to the closest anchor
-            // instead. Note that this stops any ongoing animation.
-            minBound = Float.NEGATIVE_INFINITY
-            maxBound = Float.POSITIVE_INFINITY
-            val animationTargetValue = animationTarget.value
-            // if we're in the animation already, let's find it a new home
-            val targetOffset =
-                if (animationTargetValue != null) {
-                    // first, try to map old state to the new state
-                    val oldState = oldAnchors[animationTargetValue]
-                    val newState = newAnchors.getOffset(oldState)
-                    // return new state if exists, or find the closes one among new anchors
-                    newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
-                } else {
-                    // we're not animating, proceed by finding the new anchors for an old value
-                    val actualOldValue = oldAnchors[offset.value]
-                    val value = if (actualOldValue == currentValue) currentValue else actualOldValue
-                    newAnchors.getOffset(value)
-                        ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
-                }
-            try {
-                animateInternalToOffset(targetOffset, animationSpec)
-            } catch (c: CancellationException) {
-                // If the animation was interrupted for any reason, snap as a last resort.
-                snapInternalToOffset(targetOffset)
-            } finally {
-                currentValue = newAnchors.getValue(targetOffset)
-                minBound = newAnchors.keys.minOrNull()!!
-                maxBound = newAnchors.keys.maxOrNull()!!
-            }
-        }
-    }
-
-    internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
-
-    internal var velocityThreshold by mutableStateOf(0f)
-
-    internal var resistance: ResistanceConfig? by mutableStateOf(null)
-
-    internal val draggableState = DraggableState {
-        val newAbsolute = absoluteOffset.value + it
-        val clamped = newAbsolute.coerceIn(minBound, maxBound)
-        val overflow = newAbsolute - clamped
-        val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
-        offsetState.value = clamped + resistanceDelta
-        overflowState.value = overflow
-        absoluteOffset.value = newAbsolute
-    }
-
-    private suspend fun snapInternalToOffset(target: Float) {
-        draggableState.drag { dragBy(target - absoluteOffset.value) }
-    }
-
-    private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
-        draggableState.drag {
-            var prevValue = absoluteOffset.value
-            animationTarget.value = target
-            isAnimationRunning = true
-            try {
-                Animatable(prevValue).animateTo(target, spec) {
-                    dragBy(this.value - prevValue)
-                    prevValue = this.value
-                }
-            } finally {
-                animationTarget.value = null
-                isAnimationRunning = false
-            }
-        }
-    }
-
-    /**
-     * The target value of the state.
-     *
-     * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
-     * swipe finished. If an animation is running, this is the target value of that animation.
-     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
-     */
-    val targetValue: T
-        get() {
-            // TODO(calintat): Track current velocity (b/149549482) and use that here.
-            val target =
-                animationTarget.value
-                    ?: computeTarget(
-                        offset = offset.value,
-                        lastValue = anchors.getOffset(currentValue) ?: offset.value,
-                        anchors = anchors.keys,
-                        thresholds = thresholds,
-                        velocity = 0f,
-                        velocityThreshold = Float.POSITIVE_INFINITY
-                    )
-            return anchors[target] ?: currentValue
-        }
-
-    /**
-     * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
-     *
-     * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
-     */
-    val progress: SwipeProgress<T>
-        get() {
-            val bounds = findBounds(offset.value, anchors.keys)
-            val from: T
-            val to: T
-            val fraction: Float
-            when (bounds.size) {
-                0 -> {
-                    from = currentValue
-                    to = currentValue
-                    fraction = 1f
-                }
-                1 -> {
-                    from = anchors.getValue(bounds[0])
-                    to = anchors.getValue(bounds[0])
-                    fraction = 1f
-                }
-                else -> {
-                    val (a, b) =
-                        if (direction > 0f) {
-                            bounds[0] to bounds[1]
-                        } else {
-                            bounds[1] to bounds[0]
-                        }
-                    from = anchors.getValue(a)
-                    to = anchors.getValue(b)
-                    fraction = (offset.value - a) / (b - a)
-                }
-            }
-            return SwipeProgress(from, to, fraction)
-        }
-
-    /**
-     * The direction in which the [swipeable] is moving, relative to the current [currentValue].
-     *
-     * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
-     * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
-     */
-    val direction: Float
-        get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
-
-    /**
-     * Set the state without any animation and suspend until it's set
-     *
-     * @param targetValue The new target value to set [currentValue] to.
-     */
-    suspend fun snapTo(targetValue: T) {
-        latestNonEmptyAnchorsFlow.collect { anchors ->
-            val targetOffset = anchors.getOffset(targetValue)
-            requireNotNull(targetOffset) { "The target value must have an associated anchor." }
-            snapInternalToOffset(targetOffset)
-            currentValue = targetValue
-        }
-    }
-
-    /**
-     * Set the state to the target value by starting an animation.
-     *
-     * @param targetValue The new value to animate to.
-     * @param anim The animation that will be used to animate to the new value.
-     */
-    suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
-        latestNonEmptyAnchorsFlow.collect { anchors ->
-            try {
-                val targetOffset = anchors.getOffset(targetValue)
-                requireNotNull(targetOffset) { "The target value must have an associated anchor." }
-                animateInternalToOffset(targetOffset, anim)
-            } finally {
-                val endOffset = absoluteOffset.value
-                val endValue =
-                    anchors
-                        // fighting rounding error once again, anchor should be as close as 0.5
-                        // pixels
-                        .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
-                        .values
-                        .firstOrNull()
-                        ?: currentValue
-                currentValue = endValue
-            }
-        }
-    }
-
-    /**
-     * Perform fling with settling to one of the anchors which is determined by the given
-     * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
-     * since it will settle at the anchor.
-     *
-     * In general cases, [swipeable] flings by itself when being swiped. This method is to be used
-     * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
-     * trigger settling fling when the child scroll container reaches the bound.
-     *
-     * @param velocity velocity to fling and settle with
-     * @return the reason fling ended
-     */
-    suspend fun performFling(velocity: Float) {
-        latestNonEmptyAnchorsFlow.collect { anchors ->
-            val lastAnchor = anchors.getOffset(currentValue)!!
-            val targetValue =
-                computeTarget(
-                    offset = offset.value,
-                    lastValue = lastAnchor,
-                    anchors = anchors.keys,
-                    thresholds = thresholds,
-                    velocity = velocity,
-                    velocityThreshold = velocityThreshold
-                )
-            val targetState = anchors[targetValue]
-            if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
-            // If the user vetoed the state change, rollback to the previous state.
-            else animateInternalToOffset(lastAnchor, animationSpec)
-        }
-    }
-
-    /**
-     * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
-     * gesture flow.
-     *
-     * Note: This method performs generic drag and it won't settle to any particular anchor, *
-     * leaving swipeable in between anchors. When done dragging, [performFling] must be called as
-     * well to ensure swipeable will settle at the anchor.
-     *
-     * In general cases, [swipeable] drags by itself when being swiped. This method is to be used
-     * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
-     * force drag when the child scroll container reaches the bound.
-     *
-     * @param delta delta in pixels to drag by
-     * @return the amount of [delta] consumed
-     */
-    fun performDrag(delta: Float): Float {
-        val potentiallyConsumed = absoluteOffset.value + delta
-        val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
-        val deltaToConsume = clamped - absoluteOffset.value
-        if (abs(deltaToConsume) > 0) {
-            draggableState.dispatchRawDelta(deltaToConsume)
-        }
-        return deltaToConsume
-    }
-
-    companion object {
-        /** The default [Saver] implementation for [SwipeableState]. */
-        fun <T : Any> Saver(
-            animationSpec: AnimationSpec<Float>,
-            confirmStateChange: (T) -> Boolean
-        ) =
-            Saver<SwipeableState<T>, T>(
-                save = { it.currentValue },
-                restore = { SwipeableState(it, animationSpec, confirmStateChange) }
-            )
-    }
-}
-
-/**
- * Collects information about the ongoing swipe or animation in [swipeable].
- *
- * To access this information, use [SwipeableState.progress].
- *
- * @param from The state corresponding to the anchor we are moving away from.
- * @param to The state corresponding to the anchor we are moving towards.
- * @param fraction The fraction that the current position represents between [from] and [to]. Must
- *   be between `0` and `1`.
- */
-@Immutable
-class SwipeProgress<T>(
-    val from: T,
-    val to: T,
-    /*@FloatRange(from = 0.0, to = 1.0)*/
-    val fraction: Float
-) {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is SwipeProgress<*>) return false
-
-        if (from != other.from) return false
-        if (to != other.to) return false
-        if (fraction != other.fraction) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = from?.hashCode() ?: 0
-        result = 31 * result + (to?.hashCode() ?: 0)
-        result = 31 * result + fraction.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
-    }
-}
-
-/**
- * Create and [remember] a [SwipeableState] with the default animation clock.
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- */
-@Composable
-fun <T : Any> rememberSwipeableState(
-    initialValue: T,
-    animationSpec: AnimationSpec<Float> = AnimationSpec,
-    confirmStateChange: (newValue: T) -> Boolean = { true }
-): SwipeableState<T> {
-    return rememberSaveable(
-        saver =
-            SwipeableState.Saver(
-                animationSpec = animationSpec,
-                confirmStateChange = confirmStateChange
-            )
-    ) {
-        SwipeableState(
-            initialValue = initialValue,
-            animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange
-        )
-    }
-}
-
-/**
- * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
- * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
- * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
- *    [value] will be notified to update their state to the new value of the [SwipeableState] by
- *    invoking [onValueChange]. If the owner does not update their state to the provided value for
- *    some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
- */
-@Composable
-internal fun <T : Any> rememberSwipeableStateFor(
-    value: T,
-    onValueChange: (T) -> Unit,
-    animationSpec: AnimationSpec<Float> = AnimationSpec
-): SwipeableState<T> {
-    val swipeableState = remember {
-        SwipeableState(
-            initialValue = value,
-            animationSpec = animationSpec,
-            confirmStateChange = { true }
-        )
-    }
-    val forceAnimationCheck = remember { mutableStateOf(false) }
-    LaunchedEffect(value, forceAnimationCheck.value) {
-        if (value != swipeableState.currentValue) {
-            swipeableState.animateTo(value)
-        }
-    }
-    DisposableEffect(swipeableState.currentValue) {
-        if (value != swipeableState.currentValue) {
-            onValueChange(swipeableState.currentValue)
-            forceAnimationCheck.value = !forceAnimationCheck.value
-        }
-        onDispose {}
-    }
-    return swipeableState
-}
-
-/**
- * Enable swipe gestures between a set of predefined states.
- *
- * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
- * this map cannot be empty and cannot have two anchors mapped to the same state.
- *
- * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
- * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
- * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableState] will also be updated to the state corresponding to the
- * new anchor. The target anchor is calculated based on the provided positional [thresholds].
- *
- * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
- * past these bounds, a resistance effect will be applied by default. The amount of resistance at
- * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
- *
- * For an example of a [swipeable] with three states, see:
- *
- * @param T The type of the state.
- * @param state The state of the [swipeable].
- * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
- * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
- *   used to determine which state to animate to when swiping stops. This is represented as a lambda
- *   that takes two states and returns the threshold between them in the form of a
- *   [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
- * @param orientation The orientation in which the [swipeable] can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
- * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
- *   will behave like bottom to top, and a left to right swipe will behave like right to left.
- * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
- *   [Modifier.draggable].
- * @param resistance Controls how much resistance will be applied when swiping past the bounds.
- * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
- *   order to animate to the next state, even if the positional [thresholds] have not been reached.
- * @sample androidx.compose.material.samples.SwipeableSample
- */
-fun <T> Modifier.swipeable(
-    state: SwipeableState<T>,
-    anchors: Map<Float, T>,
-    orientation: Orientation,
-    enabled: Boolean = true,
-    reverseDirection: Boolean = false,
-    interactionSource: MutableInteractionSource? = null,
-    thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
-    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
-    velocityThreshold: Dp = VelocityThreshold
-) =
-    composed(
-        inspectorInfo =
-            debugInspectorInfo {
-                name = "swipeable"
-                properties["state"] = state
-                properties["anchors"] = anchors
-                properties["orientation"] = orientation
-                properties["enabled"] = enabled
-                properties["reverseDirection"] = reverseDirection
-                properties["interactionSource"] = interactionSource
-                properties["thresholds"] = thresholds
-                properties["resistance"] = resistance
-                properties["velocityThreshold"] = velocityThreshold
-            }
-    ) {
-        require(anchors.isNotEmpty()) { "You must have at least one anchor." }
-        require(anchors.values.distinct().count() == anchors.size) {
-            "You cannot have two anchors mapped to the same state."
-        }
-        val density = LocalDensity.current
-        state.ensureInit(anchors)
-        LaunchedEffect(anchors, state) {
-            val oldAnchors = state.anchors
-            state.anchors = anchors
-            state.resistance = resistance
-            state.thresholds = { a, b ->
-                val from = anchors.getValue(a)
-                val to = anchors.getValue(b)
-                with(thresholds(from, to)) { density.computeThreshold(a, b) }
-            }
-            with(density) { state.velocityThreshold = velocityThreshold.toPx() }
-            state.processNewAnchors(oldAnchors, anchors)
-        }
-
-        Modifier.draggable(
-            orientation = orientation,
-            enabled = enabled,
-            reverseDirection = reverseDirection,
-            interactionSource = interactionSource,
-            startDragImmediately = state.isAnimationRunning,
-            onDragStopped = { velocity -> launch { state.performFling(velocity) } },
-            state = state.draggableState
-        )
-    }
-
-/**
- * Interface to compute a threshold between two anchors/states in a [swipeable].
- *
- * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
- */
-@Stable
-interface ThresholdConfig {
-    /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
-    fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
-}
-
-/**
- * A fixed threshold will be at an [offset] away from the first anchor.
- *
- * @param offset The offset (in dp) that the threshold will be at.
- */
-@Immutable
-data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
-    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
-        return fromValue + offset.toPx() * sign(toValue - fromValue)
-    }
-}
-
-/**
- * A fractional threshold will be at a [fraction] of the way between the two anchors.
- *
- * @param fraction The fraction (between 0 and 1) that the threshold will be at.
- */
-@Immutable
-data class FractionalThreshold(
-    /*@FloatRange(from = 0.0, to = 1.0)*/
-    private val fraction: Float
-) : ThresholdConfig {
-    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
-        return lerp(fromValue, toValue, fraction)
-    }
-}
-
-/**
- * Specifies how resistance is calculated in [swipeable].
- *
- * There are two things needed to calculate resistance: the resistance basis determines how much
- * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
- * amount of resistance (the larger the resistance factor, the stronger the resistance).
- *
- * The resistance basis is usually either the size of the component which [swipeable] is applied to,
- * or the distance between the minimum and maximum anchors. For a constructor in which the
- * resistance basis defaults to the latter, consider using [resistanceConfig].
- *
- * You may specify different resistance factors for each bound. Consider using one of the default
- * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
- * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
- * right now. Also, you can set either factor to 0 to disable resistance at that bound.
- *
- * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
- * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
- *   negative.
- * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
- *   negative.
- */
-@Immutable
-class ResistanceConfig(
-    /*@FloatRange(from = 0.0, fromInclusive = false)*/
-    val basis: Float,
-    /*@FloatRange(from = 0.0)*/
-    val factorAtMin: Float = StandardResistanceFactor,
-    /*@FloatRange(from = 0.0)*/
-    val factorAtMax: Float = StandardResistanceFactor
-) {
-    fun computeResistance(overflow: Float): Float {
-        val factor = if (overflow < 0) factorAtMin else factorAtMax
-        if (factor == 0f) return 0f
-        val progress = (overflow / basis).coerceIn(-1f, 1f)
-        return basis / factor * sin(progress * PI.toFloat() / 2)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ResistanceConfig) return false
-
-        if (basis != other.basis) return false
-        if (factorAtMin != other.factorAtMin) return false
-        if (factorAtMax != other.factorAtMax) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = basis.hashCode()
-        result = 31 * result + factorAtMin.hashCode()
-        result = 31 * result + factorAtMax.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
-    }
-}
-
-/**
- * Given an offset x and a set of anchors, return a list of anchors:
- * 1. [ ] if the set of anchors is empty,
- * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
- *    x rounded to the exact value of the matching anchor,
- * 3. [ min ] if min is the minimum anchor and x < min,
- * 4. [ max ] if max is the maximum anchor and x > max, or
- * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
- */
-private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
-    // Find the anchors the target lies between with a little bit of rounding error.
-    val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
-    val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
-
-    return when {
-        a == null ->
-            // case 1 or 3
-            listOfNotNull(b)
-        b == null ->
-            // case 4
-            listOf(a)
-        a == b ->
-            // case 2
-            // Can't return offset itself here since it might not be exactly equal
-            // to the anchor, despite being considered an exact match.
-            listOf(a)
-        else ->
-            // case 5
-            listOf(a, b)
-    }
-}
-
-private fun computeTarget(
-    offset: Float,
-    lastValue: Float,
-    anchors: Set<Float>,
-    thresholds: (Float, Float) -> Float,
-    velocity: Float,
-    velocityThreshold: Float
-): Float {
-    val bounds = findBounds(offset, anchors)
-    return when (bounds.size) {
-        0 -> lastValue
-        1 -> bounds[0]
-        else -> {
-            val lower = bounds[0]
-            val upper = bounds[1]
-            if (lastValue <= offset) {
-                // Swiping from lower to upper (positive).
-                if (velocity >= velocityThreshold) {
-                    return upper
-                } else {
-                    val threshold = thresholds(lower, upper)
-                    if (offset < threshold) lower else upper
-                }
-            } else {
-                // Swiping from upper to lower (negative).
-                if (velocity <= -velocityThreshold) {
-                    return lower
-                } else {
-                    val threshold = thresholds(upper, lower)
-                    if (offset > threshold) upper else lower
-                }
-            }
-        }
-    }
-}
-
-private fun <T> Map<Float, T>.getOffset(state: T): Float? {
-    return entries.firstOrNull { it.value == state }?.key
-}
-
-/** Contains useful defaults for [swipeable] and [SwipeableState]. */
-object SwipeableDefaults {
-    /** The default animation used by [SwipeableState]. */
-    val AnimationSpec = SpringSpec<Float>()
-
-    /** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */
-    val VelocityThreshold = 125.dp
-
-    /** A stiff resistance factor which indicates that swiping isn't available right now. */
-    const val StiffResistanceFactor = 20f
-
-    /** A standard resistance factor which indicates that the user has run out of things to see. */
-    const val StandardResistanceFactor = 10f
-
-    /**
-     * The default resistance config used by [swipeable].
-     *
-     * This returns `null` if there is one anchor. If there are at least two anchors, it returns a
-     * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
-     */
-    fun resistanceConfig(
-        anchors: Set<Float>,
-        factorAtMin: Float = StandardResistanceFactor,
-        factorAtMax: Float = StandardResistanceFactor
-    ): ResistanceConfig? {
-        return if (anchors.size <= 1) {
-            null
-        } else {
-            val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
-            ResistanceConfig(basis, factorAtMin, factorAtMax)
-        }
-    }
-}
-
-// temp default nested scroll connection for swipeables which desire as an opt in
-// revisit in b/174756744 as all types will have their own specific connection probably
-internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
-    get() =
-        object : NestedScrollConnection {
-            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-                val delta = available.toFloat()
-                return if (delta < 0 && source == NestedScrollSource.Drag) {
-                    performDrag(delta).toOffset()
-                } else {
-                    Offset.Zero
-                }
-            }
-
-            override fun onPostScroll(
-                consumed: Offset,
-                available: Offset,
-                source: NestedScrollSource
-            ): Offset {
-                return if (source == NestedScrollSource.Drag) {
-                    performDrag(available.toFloat()).toOffset()
-                } else {
-                    Offset.Zero
-                }
-            }
-
-            override suspend fun onPreFling(available: Velocity): Velocity {
-                val toFling = Offset(available.x, available.y).toFloat()
-                return if (toFling < 0 && offset.value > minBound) {
-                    performFling(velocity = toFling)
-                    // since we go to the anchor with tween settling, consume all for the best UX
-                    available
-                } else {
-                    Velocity.Zero
-                }
-            }
-
-            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
-                performFling(velocity = Offset(available.x, available.y).toFloat())
-                return available
-            }
-
-            private fun Float.toOffset(): Offset = Offset(0f, this)
-
-            private fun Offset.toFloat(): Float = this.y
-        }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
new file mode 100644
index 0000000..741f00d
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.util
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+/**
+ * Iterates through a [List] using the index and calls [action] for each item. This does not
+ * allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+    contract { callsInPlace(action) }
+    for (index in indices) {
+        val item = get(index)
+        action(item)
+    }
+}
+
+/**
+ * Returns a list containing the results of applying the given [transform] function to each element
+ * in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+    contract { callsInPlace(transform) }
+    val target = ArrayList<R>(size)
+    fastForEach { target += transform(it) }
+    return target
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
index c1defb7..eb1a634 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
@@ -17,11 +17,10 @@
 
 package com.android.compose.ui.util
 
+import androidx.compose.ui.unit.IntSize
 import kotlin.math.roundToInt
 import kotlin.math.roundToLong
 
-// TODO(b/272311106): this is a fork from material. Unfork it when MathHelpers.kt reaches material3.
-
 /** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
 fun lerp(start: Float, stop: Float, fraction: Float): Float {
     return (1 - fraction) * start + fraction * stop
@@ -36,3 +35,11 @@
 fun lerp(start: Long, stop: Long, fraction: Float): Long {
     return start + ((stop - start) * fraction.toDouble()).roundToLong()
 }
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: IntSize, stop: IntSize, fraction: Float): IntSize {
+    return IntSize(
+        lerp(start.width, stop.width, fraction),
+        lerp(start.height, stop.height, fraction)
+    )
+}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
index 06d94ac..5a8a374 100644
--- a/packages/SystemUI/compose/core/tests/Android.bp
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -42,6 +42,8 @@
         "androidx.compose.runtime_runtime",
         "androidx.compose.ui_ui-test-junit4",
         "androidx.compose.ui_ui-test-manifest",
+
+        "truth-prebuilt",
     ],
 
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
new file mode 100644
index 0000000..04b3f8a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ObservableTransitionStateTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun testObservableTransitionState() = runTest {
+        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+
+        // Collect the current observable state into [observableState].
+        // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be
+        // reused by non-SystemUI testing code.
+        var observableState: ObservableTransitionState? = null
+        backgroundScope.launch {
+            state.observableTransitionState().collect { observableState = it }
+        }
+
+        fun observableState(): ObservableTransitionState {
+            runCurrent()
+            return observableState!!
+        }
+
+        fun ObservableTransitionState.Transition.progress(): Float {
+            var lastProgress = -1f
+            backgroundScope.launch { progress.collect { lastProgress = it } }
+            runCurrent()
+            return lastProgress
+        }
+
+        rule.testTransition(
+            from = TestScenes.SceneA,
+            to = TestScenes.SceneB,
+            transitionLayout = { currentScene, onChangeScene ->
+                SceneTransitionLayout(
+                    currentScene,
+                    onChangeScene,
+                    EmptyTestTransitions,
+                    state = state,
+                ) {
+                    scene(TestScenes.SceneA) {}
+                    scene(TestScenes.SceneB) {}
+                }
+            }
+        ) {
+            before {
+                assertThat(observableState())
+                    .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneA))
+            }
+            at(0) {
+                val state = observableState()
+                assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java)
+                assertThat((state as ObservableTransitionState.Transition).fromScene)
+                    .isEqualTo(TestScenes.SceneA)
+                assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+                assertThat(state.progress()).isEqualTo(0f)
+            }
+            at(TestTransitionDuration / 2) {
+                val state = observableState()
+                assertThat((state as ObservableTransitionState.Transition).fromScene)
+                    .isEqualTo(TestScenes.SceneA)
+                assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+                assertThat(state.progress()).isEqualTo(0.5f)
+            }
+            after {
+                assertThat(observableState())
+                    .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneB))
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
new file mode 100644
index 0000000..8bd6545
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.ComponentActivity
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.subjects.DpOffsetSubject
+import com.android.compose.test.subjects.assertThat
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SceneTransitionLayoutTest {
+    companion object {
+        private val LayoutSize = 300.dp
+    }
+
+    private var currentScene by mutableStateOf(TestScenes.SceneA)
+    private val layoutState = SceneTransitionLayoutState(currentScene)
+
+    // We use createAndroidComposeRule() here and not createComposeRule() because we need an
+    // activity for testBack().
+    @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+    /** The content under test. */
+    @Composable
+    private fun TestContent() {
+        SceneTransitionLayout(
+            currentScene,
+            { currentScene = it },
+            EmptyTestTransitions,
+            state = layoutState,
+            modifier = Modifier.size(LayoutSize),
+        ) {
+            scene(
+                TestScenes.SceneA,
+                userActions = mapOf(Back to TestScenes.SceneB),
+            ) {
+                Box(Modifier.fillMaxSize()) {
+                    SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
+                    Text("SceneA")
+                }
+            }
+            scene(TestScenes.SceneB) {
+                Box(Modifier.fillMaxSize()) {
+                    SharedFoo(
+                        size = 100.dp,
+                        childOffset = 50.dp,
+                        Modifier.align(Alignment.TopStart),
+                    )
+                    Text("SceneB")
+                }
+            }
+            scene(TestScenes.SceneC) {
+                Box(Modifier.fillMaxSize()) {
+                    SharedFoo(
+                        size = 150.dp,
+                        childOffset = 100.dp,
+                        Modifier.align(Alignment.BottomStart),
+                    )
+                    Text("SceneC")
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+        Box(
+            modifier
+                .size(size)
+                .background(Color.Red)
+                .element(TestElements.Foo)
+                .testTag(TestElements.Foo.name)
+        ) {
+            // Offset the single child of Foo by some animated shared offset.
+            val offset by animateSharedDpAsState(childOffset, TestValues.Value1, TestElements.Foo)
+
+            Box(
+                Modifier.offset {
+                        val pxOffset = offset.roundToPx()
+                        IntOffset(pxOffset, pxOffset)
+                    }
+                    .size(30.dp)
+                    .background(Color.Blue)
+                    .testTag(TestElements.Bar.name)
+            )
+        }
+    }
+
+    @Test
+    fun testOnlyCurrentSceneIsDisplayed() {
+        rule.setContent { TestContent() }
+
+        // Only scene A is displayed.
+        rule.onNodeWithText("SceneA").assertIsDisplayed()
+        rule.onNodeWithText("SceneB").assertDoesNotExist()
+        rule.onNodeWithText("SceneC").assertDoesNotExist()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Change to scene B. Only that scene is displayed.
+        currentScene = TestScenes.SceneB
+        rule.onNodeWithText("SceneA").assertDoesNotExist()
+        rule.onNodeWithText("SceneB").assertIsDisplayed()
+        rule.onNodeWithText("SceneC").assertDoesNotExist()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+    }
+
+    @Test
+    fun testBack() {
+        rule.setContent { TestContent() }
+
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        rule.activity.onBackPressed()
+        rule.waitForIdle()
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+    }
+
+    @Test
+    fun testTransitionState() {
+        rule.setContent { TestContent() }
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // We will advance the clock manually.
+        rule.mainClock.autoAdvance = false
+
+        // Change the current scene. Until composition is triggered, this won't change the layout
+        // state.
+        currentScene = TestScenes.SceneB
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // On the next frame, we will recompose because currentScene changed, which will start the
+        // transition (i.e. it will change the transitionState to be a Transition) in a
+        // LaunchedEffect.
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+        val transition = layoutState.transitionState as TransitionState.Transition
+        assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        assertThat(transition.progress).isEqualTo(0f)
+
+        // Then, on the next frame, the animator we started gets its initial value and clock
+        // starting time. We are now at progress = 0f.
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+            .isEqualTo(0f)
+
+        // The test transition lasts 480ms. 240ms after the start of the transition, we are at
+        // progress = 0.5f.
+        rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+            .isEqualTo(0.5f)
+
+        // (240-16) ms later, i.e. one frame before the transition is finished, we are at
+        // progress=(480-16)/480.
+        rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16)
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+            .isEqualTo((TestTransitionDuration - 16) / 480f)
+
+        // one frame (16ms) later, the transition is finished and we are in the idle state in scene
+        // B.
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+    }
+
+    @Test
+    fun testSharedElement() {
+        rule.setContent { TestContent() }
+
+        // In scene A, the shared element SharedFoo() is at the top end of the layout and has a size
+        // of 50.dp.
+        var sharedFoo = rule.onNodeWithTag(TestElements.Foo.name, useUnmergedTree = true)
+        sharedFoo.assertWidthIsEqualTo(50.dp)
+        sharedFoo.assertHeightIsEqualTo(50.dp)
+        sharedFoo.assertPositionInRootIsEqualTo(
+            expectedTop = 0.dp,
+            expectedLeft = LayoutSize - 50.dp,
+        )
+
+        // The shared offset of the single child of SharedFoo() is 0dp in scene A.
+        assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo)).isEqualTo(DpOffset(0.dp, 0.dp))
+
+        // Pause animations to test the state mid-transition.
+        rule.mainClock.autoAdvance = false
+
+        // Go to scene B and let the animation start. See [testLayoutState()] and
+        // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock
+        // by 2 frames to be at the start of the animation.
+        currentScene = TestScenes.SceneB
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+
+        // Advance to the middle of the animation.
+        rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+        // We need to use onAllNodesWithTag().onFirst() here given that shared elements are
+        // composed and laid out in both scenes (but drawn only in one).
+        sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+
+        // In scene B, foo is at the top start (x = 0, y = 0) of the layout and has a size of
+        // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
+        // use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
+        // going to (x = 0, y = 0), so the offset should now be half what it was.
+        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+            .isEqualTo(0.5f)
+        sharedFoo.assertWidthIsEqualTo(75.dp)
+        sharedFoo.assertHeightIsEqualTo(75.dp)
+        sharedFoo.assertPositionInRootIsEqualTo(
+            expectedTop = 0.dp,
+            expectedLeft = (LayoutSize - 50.dp) / 2
+        )
+
+        // The shared offset of the single child of SharedFoo() is 50dp in scene B and 0dp in Scene
+        // A, so it should be 25dp now.
+        assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+            .isWithin(DpOffsetSubject.DefaultTolerance)
+            .of(DpOffset(25.dp, 25.dp))
+
+        // Animate to scene C, let the animation start then go to the middle of the transition.
+        currentScene = TestScenes.SceneC
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+        // In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The
+        // transition scene B => scene C is using a FastOutSlowIn interpolator.
+        val interpolatedProgress = FastOutSlowInEasing.transform(0.5f)
+        val expectedTop = (LayoutSize - 150.dp) * interpolatedProgress
+        val expectedLeft = 0.dp
+        val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
+
+        sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+            .isEqualTo(interpolatedProgress)
+        sharedFoo.assertWidthIsEqualTo(expectedSize)
+        sharedFoo.assertHeightIsEqualTo(expectedSize)
+        sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+
+        // The shared offset of the single child of SharedFoo() is 50dp in scene B and 100dp in
+        // Scene C.
+        val expectedOffset = 50.dp + (100.dp - 50.dp) * interpolatedProgress
+        assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+            .isWithin(DpOffsetSubject.DefaultTolerance)
+            .of(DpOffset(expectedOffset, expectedOffset))
+
+        // Go back to scene A. This should happen instantly (once the animation started, i.e. after
+        // 2 frames) given that we use a snap() animation spec.
+        currentScene = TestScenes.SceneA
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+    }
+
+    private fun SemanticsNodeInteraction.offsetRelativeTo(
+        other: SemanticsNodeInteraction,
+    ): DpOffset {
+        val node = fetchSemanticsNode()
+        val bounds = node.boundsInRoot
+        val otherBounds = other.fetchSemanticsNode().boundsInRoot
+        return with(node.layoutInfo.density) {
+            DpOffset(
+                x = (bounds.left - otherBounds.left).toDp(),
+                y = (bounds.top - otherBounds.top).toDp(),
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
new file mode 100644
index 0000000..cb2607a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SwipeToSceneTest {
+    companion object {
+        private val LayoutWidth = 200.dp
+        private val LayoutHeight = 400.dp
+
+        /** The middle of the layout, in pixels. */
+        private val Density.middle: Offset
+            get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
+    }
+
+    private var currentScene by mutableStateOf(TestScenes.SceneA)
+    private val layoutState = SceneTransitionLayoutState(currentScene)
+
+    @get:Rule val rule = createComposeRule()
+
+    /** The content under test. */
+    @Composable
+    private fun TestContent() {
+        SceneTransitionLayout(
+            currentScene,
+            { currentScene = it },
+            EmptyTestTransitions,
+            state = layoutState,
+            modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.name),
+        ) {
+            scene(
+                TestScenes.SceneA,
+                userActions =
+                    mapOf(
+                        Swipe.Left to TestScenes.SceneB,
+                        Swipe.Down to TestScenes.SceneC,
+                    ),
+            ) {
+                Box(Modifier.fillMaxSize())
+            }
+            scene(
+                TestScenes.SceneB,
+                userActions = mapOf(Swipe.Right to TestScenes.SceneA),
+            ) {
+                Box(Modifier.fillMaxSize())
+            }
+            scene(
+                TestScenes.SceneC,
+                userActions = mapOf(Swipe.Down to TestScenes.SceneA),
+            ) {
+                Box(Modifier.fillMaxSize())
+            }
+        }
+    }
+
+    @Test
+    fun testDragWithPositionalThreshold() {
+        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+        // detected as a drag event.
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            TestContent()
+        }
+
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the
+        // positional threshold from which we commit the gesture.
+        rule.onRoot().performTouchInput {
+            down(middle)
+
+            // We use a high delay so that the velocity of the gesture is slow (otherwise it would
+            // commit the gesture, even if we are below the positional threshold).
+            moveBy(Offset(-55.dp.toPx() - touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
+        // the gesture axis as swipe distance.
+        var transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+        // Release the finger. We should now be animating back to A (currentScene = SceneA) given
+        // that 55dp < positional threshold.
+        rule.onRoot().performTouchInput { up() }
+        transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+        // Wait for the animation to finish. We should now be in scene A.
+        rule.waitForIdle()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Now we do the same but vertically and with a drag distance of 56dp, which is >=
+        // positional threshold.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, 56.dp.toPx() + touchSlop), delayMillis = 1_000)
+        }
+
+        // Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
+        transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+        // Release the finger. We should now be animating to C (currentScene = SceneC) given
+        // that 56dp >= positional threshold.
+        rule.onRoot().performTouchInput { up() }
+        transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+        // Wait for the animation to finish. We should now be in scene C.
+        rule.waitForIdle()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+    }
+
+    @Test
+    fun testSwipeWithVelocityThreshold() {
+        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+        // detected as a drag event.
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            TestContent()
+        }
+
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here
+        // because 125 dp/s is the velocity threshold from which we commit the gesture. We also use
+        // a swipe distance < 56dp, the positional threshold, to make sure that we don't commit
+        // the gesture because of a large enough swipe distance.
+        rule.onRoot().performTouchInput {
+            swipeWithVelocity(
+                start = middle,
+                end = middle - Offset(55.dp.toPx() + touchSlop, 0f),
+                endVelocity = 124.dp.toPx(),
+            )
+        }
+
+        // We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
+        // threshold.
+        var transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+        // Wait for the animation to finish. We should now be in scene A.
+        rule.waitForIdle()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Now we do the same but vertically and with a swipe velocity of 126dp, which is >
+        // velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold)
+        // but it doesn't work reliably with how swipeWithVelocity() computes move events to get to
+        // the target velocity, probably because of float rounding errors.
+        rule.onRoot().performTouchInput {
+            swipeWithVelocity(
+                start = middle,
+                end = middle + Offset(0f, 55.dp.toPx() + touchSlop),
+                endVelocity = 126.dp.toPx(),
+            )
+        }
+
+        // We should be animating to C (currentScene = SceneC).
+        transition = layoutState.transitionState
+        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+        assertThat((transition as TransitionState.Transition).fromScene)
+            .isEqualTo(TestScenes.SceneA)
+        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight)
+
+        // Wait for the animation to finish. We should now be in scene C.
+        rule.waitForIdle()
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
new file mode 100644
index 0000000..275149a0
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+
+@DslMarker annotation class TransitionTestDsl
+
+@TransitionTestDsl
+interface TransitionTestBuilder {
+    /**
+     * Assert on the state of the layout before the transition starts.
+     *
+     * This should be called maximum once, before [at] or [after] is called.
+     */
+    fun before(builder: TransitionTestAssertionScope.() -> Unit)
+
+    /**
+     * Assert on the state of the layout during the transition at [timestamp].
+     *
+     * This should be called after [before] is called and before [after] is called. Successive calls
+     * to [at] must be called with increasing [timestamp].
+     *
+     * Important: [timestamp] must be a multiple of 16 (the duration of a frame on the JVM/Android).
+     * There is no intermediary state between `t` and `t + 16` , so testing transitions outside of
+     * `t = 0`, `t = 16`, `t = 32`, etc does not make sense.
+     */
+    fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
+
+    /**
+     * Assert on the state of the layout after the transition finished.
+     *
+     * This should be called maximum once, after [before] or [at] is called.
+     */
+    fun after(builder: TransitionTestAssertionScope.() -> Unit)
+}
+
+@TransitionTestDsl
+interface TransitionTestAssertionScope {
+    /** Assert on [element]. */
+    fun onElement(element: ElementKey): SemanticsNodeInteraction
+}
+
+/**
+ * Test the transition between [fromSceneContent] and [toSceneContent] at different points in time.
+ *
+ * @sample com.android.compose.animation.scene.transformation.TranslateTest
+ */
+fun ComposeContentTestRule.testTransition(
+    fromSceneContent: @Composable SceneScope.() -> Unit,
+    toSceneContent: @Composable SceneScope.() -> Unit,
+    transition: TransitionBuilder.() -> Unit,
+    layoutModifier: Modifier = Modifier,
+    builder: TransitionTestBuilder.() -> Unit,
+) {
+    testTransition(
+        from = TestScenes.SceneA,
+        to = TestScenes.SceneB,
+        transitionLayout = { currentScene, onChangeScene ->
+            SceneTransitionLayout(
+                currentScene,
+                onChangeScene,
+                transitions { from(TestScenes.SceneA, to = TestScenes.SceneB, transition) },
+                layoutModifier.fillMaxSize(),
+            ) {
+                scene(TestScenes.SceneA, content = fromSceneContent)
+                scene(TestScenes.SceneB, content = toSceneContent)
+            }
+        },
+        builder,
+    )
+}
+
+/**
+ * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different
+ * points in time.
+ */
+fun ComposeContentTestRule.testTransition(
+    from: SceneKey,
+    to: SceneKey,
+    transitionLayout:
+        @Composable
+        (
+            currentScene: SceneKey,
+            onChangeScene: (SceneKey) -> Unit,
+        ) -> Unit,
+    builder: TransitionTestBuilder.() -> Unit,
+) {
+    val test = transitionTest(builder)
+    val assertionScope =
+        object : TransitionTestAssertionScope {
+            override fun onElement(element: ElementKey): SemanticsNodeInteraction {
+                return this@testTransition.onNodeWithTag(element.name)
+            }
+        }
+
+    var currentScene by mutableStateOf(from)
+    setContent { transitionLayout(currentScene, { currentScene = it }) }
+
+    // Wait for the UI to be idle then test the before state.
+    waitForIdle()
+    test.before(assertionScope)
+
+    // Manually advance the clock to the start of the animation.
+    mainClock.autoAdvance = false
+
+    // Change the current scene.
+    currentScene = to
+
+    // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will
+    // change the transitionState to be a Transition) in a LaunchedEffect.
+    mainClock.advanceTimeByFrame()
+
+    // Advance by another frame so that the animator we started gets its initial value and clock
+    // starting time. We are now at progress = 0f.
+    mainClock.advanceTimeByFrame()
+    waitForIdle()
+
+    // Test the assertions at specific points in time.
+    test.timestamps.forEach { tsAssertion ->
+        if (tsAssertion.timestampDelta > 0L) {
+            mainClock.advanceTimeBy(tsAssertion.timestampDelta)
+            waitForIdle()
+        }
+
+        tsAssertion.assertion(assertionScope)
+    }
+
+    // Go to the end state and test it.
+    mainClock.autoAdvance = true
+    waitForIdle()
+    test.after(assertionScope)
+}
+
+private fun transitionTest(builder: TransitionTestBuilder.() -> Unit): TransitionTest {
+    // Collect the assertion lambdas in [TransitionTest]. Note that the ordering is forced by the
+    // builder, e.g. `before {}` must be called before everything else, then `at {}` (in increasing
+    // order of timestamp), then `after {}`. That way the test code is run with the same order as it
+    // is written, to avoid confusion.
+
+    val impl =
+        object : TransitionTestBuilder {
+                var before: (TransitionTestAssertionScope.() -> Unit)? = null
+                var after: (TransitionTestAssertionScope.() -> Unit)? = null
+                val timestamps = mutableListOf<TimestampAssertion>()
+
+                private var currentTimestamp = 0L
+
+                override fun before(builder: TransitionTestAssertionScope.() -> Unit) {
+                    check(before == null) { "before {} must be called maximum once" }
+                    check(after == null) { "before {} must be called before after {}" }
+                    check(timestamps.isEmpty()) { "before {} must be called before at(...) {}" }
+
+                    before = builder
+                }
+
+                override fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit) {
+                    check(after == null) { "at(...) {} must be called before after {}" }
+                    check(timestamp >= currentTimestamp) {
+                        "at(...) must be called with timestamps in increasing order"
+                    }
+                    check(timestamp % 16 == 0L) {
+                        "timestamp must be a multiple of the frame time (16ms)"
+                    }
+
+                    val delta = timestamp - currentTimestamp
+                    currentTimestamp = timestamp
+
+                    timestamps.add(TimestampAssertion(delta, builder))
+                }
+
+                override fun after(builder: TransitionTestAssertionScope.() -> Unit) {
+                    check(after == null) { "after {} must be called maximum once" }
+                    after = builder
+                }
+            }
+            .apply(builder)
+
+    return TransitionTest(
+        before = impl.before ?: {},
+        timestamps = impl.timestamps,
+        after = impl.after ?: {},
+    )
+}
+
+private class TransitionTest(
+    val before: TransitionTestAssertionScope.() -> Unit,
+    val after: TransitionTestAssertionScope.() -> Unit,
+    val timestamps: List<TimestampAssertion>,
+)
+
+private class TimestampAssertion(
+    val timestampDelta: Long,
+    val assertion: TransitionTestAssertionScope.() -> Unit,
+)
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
new file mode 100644
index 0000000..8357262
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+
+/** Scenes keys that can be reused by tests. */
+object TestScenes {
+    val SceneA = SceneKey("SceneA")
+    val SceneB = SceneKey("SceneB")
+    val SceneC = SceneKey("SceneC")
+}
+
+/** Element keys that can be reused by tests. */
+object TestElements {
+    val Foo = ElementKey("Foo")
+    val Bar = ElementKey("Bar")
+}
+
+/** Value keys that can be reused by tests. */
+object TestValues {
+    val Value1 = ValueKey("Value1")
+}
+
+// We use a transition duration of 480ms here because it is a multiple of 16, the time of a frame in
+// C JVM/Android. Doing so allows us for instance to test the state at progress = 0.5f given that t
+// = 240ms is also a multiple of 16.
+val TestTransitionDuration = 480L
+
+/** A definition of empty transitions between [TestScenes], using different animation specs. */
+val EmptyTestTransitions = transitions {
+    from(TestScenes.SceneA, to = TestScenes.SceneB) {
+        spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = LinearEasing)
+    }
+
+    from(TestScenes.SceneB, to = TestScenes.SceneC) {
+        spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = FastOutSlowInEasing)
+    }
+
+    from(TestScenes.SceneC, to = TestScenes.SceneA) { spec = snap() }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
new file mode 100644
index 0000000..8ef6757
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredSizeTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun testAnchoredSizeEnter() {
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
+            toSceneContent = {
+                Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
+                Box(Modifier.size(200.dp, 60.dp).element(TestElements.Bar))
+            },
+            transition = {
+                // Scale during 4 frames.
+                spec = tween(16 * 4, easing = LinearEasing)
+                anchoredSize(TestElements.Bar, TestElements.Foo)
+            },
+        ) {
+            // Bar is entering. It starts at the same size as Foo in scene A in and scales to its
+            // final size in scene B.
+            before { onElement(TestElements.Bar).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+            at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+            at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+            at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+            at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+            after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+        }
+    }
+
+    @Test
+    fun testAnchoredSizeExit() {
+        rule.testTransition(
+            fromSceneContent = {
+                Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo))
+                Box(Modifier.size(100.dp, 100.dp).element(TestElements.Bar))
+            },
+            toSceneContent = { Box(Modifier.size(200.dp, 60.dp).element(TestElements.Foo)) },
+            transition = {
+                // Scale during 4 frames.
+                spec = tween(16 * 4, easing = LinearEasing)
+                anchoredSize(TestElements.Bar, TestElements.Foo)
+            },
+        ) {
+            // Bar is leaving. It starts at 100dp x 100dp in scene A and is scaled to 200dp x 60dp,
+            // the size of Foo in scene B.
+            before { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+            at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+            at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+            at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+            at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+            after { onElement(TestElements.Bar).assertDoesNotExist() }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
new file mode 100644
index 0000000..d1205e7
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredTranslateTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun testAnchoredTranslateExit() {
+        rule.testTransition(
+            fromSceneContent = {
+                Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+                Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+            },
+            toSceneContent = { Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo)) },
+            transition = {
+                // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+                spec = tween(16 * 4, easing = LinearEasing)
+                anchoredTranslate(TestElements.Bar, TestElements.Foo)
+            },
+        ) {
+            // Bar moves by (20dp, -40dp), like Foo.
+            before { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+            at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+            at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(25.dp, 30.dp) }
+            at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(30.dp, 20.dp) }
+            at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(35.dp, 10.dp) }
+            after { onElement(TestElements.Bar).assertDoesNotExist() }
+        }
+    }
+
+    @Test
+    fun testAnchoredTranslateEnter() {
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo)) },
+            toSceneContent = {
+                Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo))
+                Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+            },
+            transition = {
+                // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+                spec = tween(16 * 4, easing = LinearEasing)
+                anchoredTranslate(TestElements.Bar, TestElements.Foo)
+            },
+        ) {
+            // Bar moves by (20dp, -40dp), like Foo.
+            before { onElement(TestElements.Bar).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(0.dp, 80.dp) }
+            at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(5.dp, 70.dp) }
+            at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(10.dp, 60.dp) }
+            at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(15.dp, 50.dp) }
+            at(64) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+            after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
new file mode 100644
index 0000000..2a27763
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.TransitionTestBuilder
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EdgeTranslateTest {
+
+    @get:Rule val rule = createComposeRule()
+
+    private fun testEdgeTranslate(
+        edge: Edge,
+        startsOutsideLayoutBounds: Boolean,
+        builder: TransitionTestBuilder.() -> Unit,
+    ) {
+        rule.testTransition(
+            // The layout under test is 300dp x 300dp.
+            layoutModifier = Modifier.size(300.dp),
+            fromSceneContent = {},
+            toSceneContent = {
+                // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
+                Box(Modifier.fillMaxSize()) {
+                    Box(Modifier.size(100.dp).element(TestElements.Foo).align(Alignment.Center))
+                }
+            },
+            transition = {
+                spec = tween(16 * 4, easing = LinearEasing)
+                translate(TestElements.Foo, edge, startsOutsideLayoutBounds)
+            },
+            builder = builder,
+        )
+    }
+
+    @Test
+    fun testEntersFromTop_startsOutsideLayoutBounds() {
+        testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = true) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, (-100).dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromTop_startsInsideLayoutBounds() {
+        testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = false) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 50.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromBottom_startsOutsideLayoutBounds() {
+        testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = true) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 300.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromBottom_startsInsideLayoutBounds() {
+        testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = false) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 150.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromLeft_startsOutsideLayoutBounds() {
+        testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = true) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo((-100).dp, 100.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromLeft_startsInsideLayoutBounds() {
+        testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = false) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 100.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromRight_startsOutsideLayoutBounds() {
+        testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = true) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(300.dp, 100.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+
+    @Test
+    fun testEntersFromRight_startsInsideLayoutBounds() {
+        testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = false) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(150.dp, 100.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
new file mode 100644
index 0000000..384355c
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ScaleSizeTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun testScaleSize() {
+        rule.testTransition(
+            fromSceneContent = {},
+            toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) },
+            transition = {
+                // Scale during 4 frames.
+                spec = tween(16 * 4, easing = LinearEasing)
+                scaleSize(TestElements.Foo, width = 2f, height = 0.5f)
+            },
+        ) {
+            // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the
+            // transition so it starts at 200dp x 50dp.
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(200.dp, 50.dp) }
+            at(16) { onElement(TestElements.Foo).assertSizeIsEqualTo(175.dp, 62.5.dp) }
+            at(32) { onElement(TestElements.Foo).assertSizeIsEqualTo(150.dp, 75.dp) }
+            at(48) { onElement(TestElements.Foo).assertSizeIsEqualTo(125.dp, 87.5.dp) }
+            at(64) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+            after { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
new file mode 100644
index 0000000..1d559fd
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TranslateTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun testTranslateExit() {
+        rule.testTransition(
+            fromSceneContent = {
+                // Foo is at (10dp, 50dp) and is exiting.
+                Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+            },
+            toSceneContent = {},
+            transition = {
+                // Foo is translated by (20dp, -40dp) during 4 frames.
+                spec = tween(16 * 4, easing = LinearEasing)
+                translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+            },
+        ) {
+            before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+            at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+            at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+            after { onElement(TestElements.Foo).assertDoesNotExist() }
+        }
+    }
+
+    @Test
+    fun testTranslateEnter() {
+        rule.testTransition(
+            fromSceneContent = {},
+            toSceneContent = {
+                // Foo is entering to (10dp, 50dp)
+                Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+            },
+            transition = {
+                // Foo is translated from (10dp, 50) + (20dp, -40dp) during 4 frames.
+                spec = tween(16 * 4, easing = LinearEasing)
+                translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+            },
+        ) {
+            before { onElement(TestElements.Foo).assertDoesNotExist() }
+            at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(30.dp, 10.dp) }
+            at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+            at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+            at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+            at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+            after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
new file mode 100644
index 0000000..fbd1b51
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.test
+
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.unit.Dp
+
+fun SemanticsNodeInteraction.assertSizeIsEqualTo(expectedWidth: Dp, expectedHeight: Dp) {
+    assertWidthIsEqualTo(expectedWidth)
+    assertHeightIsEqualTo(expectedHeight)
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
new file mode 100644
index 0000000..bf7bf98
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.compose.test.subjects
+
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+
+/** Assert on a [DpOffset]. */
+fun assertThat(dpOffset: DpOffset): DpOffsetSubject {
+    return assertAbout(DpOffsetSubject.dpOffsets()).that(dpOffset)
+}
+
+/** A Truth subject to assert on [DpOffset] with some tolerance. Inspired by FloatSubject. */
+class DpOffsetSubject(
+    metadata: FailureMetadata,
+    private val actual: DpOffset,
+) : Subject(metadata, actual) {
+    fun isWithin(tolerance: Dp): TolerantDpOffsetComparison {
+        return object : TolerantDpOffsetComparison {
+            override fun of(expected: DpOffset) {
+                actual.x.assertIsEqualTo(expected.x, "offset.x", tolerance)
+                actual.y.assertIsEqualTo(expected.y, "offset.y", tolerance)
+            }
+        }
+    }
+
+    interface TolerantDpOffsetComparison {
+        fun of(expected: DpOffset)
+    }
+
+    companion object {
+        val DefaultTolerance = Dp(.5f)
+
+        fun dpOffsets() =
+            Factory<DpOffsetSubject, DpOffset> { metadata, actual ->
+                DpOffsetSubject(metadata, actual)
+            }
+    }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 82fe3f2..609ea90 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -21,13 +21,11 @@
 import android.view.View
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
 
 /** The Compose facade, when Compose is *not* available. */
 object ComposeFacade : BaseComposeFacade {
@@ -53,14 +51,6 @@
         throwComposeUnavailableError()
     }
 
-    override fun createMultiShadeView(
-        context: Context,
-        viewModel: MultiShadeViewModel,
-        clock: SystemClock,
-    ): View {
-        throwComposeUnavailableError()
-    }
-
     override fun createSceneContainerView(
         context: Context,
         viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 7926f92..0ee88b9 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -23,8 +23,6 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.theme.PlatformTheme
-import com.android.systemui.multishade.ui.composable.MultiShade
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
 import com.android.systemui.people.ui.compose.PeopleScreen
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -34,7 +32,6 @@
 import com.android.systemui.scene.ui.composable.ComposableScene
 import com.android.systemui.scene.ui.composable.SceneContainer
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
 
 /** The Compose facade, when Compose is available. */
 object ComposeFacade : BaseComposeFacade {
@@ -60,23 +57,6 @@
         }
     }
 
-    override fun createMultiShadeView(
-        context: Context,
-        viewModel: MultiShadeViewModel,
-        clock: SystemClock,
-    ): View {
-        return ComposeView(context).apply {
-            setContent {
-                PlatformTheme {
-                    MultiShade(
-                        viewModel = viewModel,
-                        clock = clock,
-                    )
-                }
-            }
-        }
-    }
-
     override fun createSceneContainerView(
         context: Context,
         viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index b3d2e35..64227b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -43,10 +43,10 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.Easings
+import com.android.compose.modifiers.thenIf
 import com.android.internal.R
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
-import com.android.systemui.compose.modifiers.thenIf
 import kotlin.math.min
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -228,43 +228,45 @@
                 }
             }
     ) {
-        // Draw lines between dots.
-        selectedDots.forEachIndexed { index, dot ->
-            if (index > 0) {
-                val previousDot = selectedDots[index - 1]
-                val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
-                val startLerp = 1 - lineFadeOutAnimationProgress
-                val from = pixelOffset(previousDot, spacing, verticalOffset)
-                val to = pixelOffset(dot, spacing, verticalOffset)
-                val lerpedFrom =
-                    Offset(
-                        x = from.x + (to.x - from.x) * startLerp,
-                        y = from.y + (to.y - from.y) * startLerp,
+        if (isAnimationEnabled) {
+            // Draw lines between dots.
+            selectedDots.forEachIndexed { index, dot ->
+                if (index > 0) {
+                    val previousDot = selectedDots[index - 1]
+                    val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
+                    val startLerp = 1 - lineFadeOutAnimationProgress
+                    val from = pixelOffset(previousDot, spacing, verticalOffset)
+                    val to = pixelOffset(dot, spacing, verticalOffset)
+                    val lerpedFrom =
+                        Offset(
+                            x = from.x + (to.x - from.x) * startLerp,
+                            y = from.y + (to.y - from.y) * startLerp,
+                        )
+                    drawLine(
+                        start = lerpedFrom,
+                        end = to,
+                        cap = StrokeCap.Round,
+                        alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
+                        color = lineColor,
+                        strokeWidth = lineStrokeWidth,
                     )
-                drawLine(
-                    start = lerpedFrom,
-                    end = to,
-                    cap = StrokeCap.Round,
-                    alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
-                    color = lineColor,
-                    strokeWidth = lineStrokeWidth,
-                )
+                }
             }
-        }
 
-        // Draw the line between the most recently-selected dot and the input pointer position.
-        inputPosition?.let { lineEnd ->
-            currentDot?.let { dot ->
-                val from = pixelOffset(dot, spacing, verticalOffset)
-                val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
-                drawLine(
-                    start = from,
-                    end = lineEnd,
-                    cap = StrokeCap.Round,
-                    alpha = lineAlpha(spacing, lineLength),
-                    color = lineColor,
-                    strokeWidth = lineStrokeWidth,
-                )
+            // Draw the line between the most recently-selected dot and the input pointer position.
+            inputPosition?.let { lineEnd ->
+                currentDot?.let { dot ->
+                    val from = pixelOffset(dot, spacing, verticalOffset)
+                    val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
+                    drawLine(
+                        start = from,
+                        end = lineEnd,
+                        cap = StrokeCap.Round,
+                        alpha = lineAlpha(spacing, lineLength),
+                        color = lineColor,
+                        strokeWidth = lineStrokeWidth,
+                    )
+                }
             }
         }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 85178bc..bef0b3d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -75,7 +75,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.Easings
 import com.android.compose.grid.VerticalGrid
-import com.android.internal.R.id.image
+import com.android.compose.modifiers.thenIf
 import com.android.systemui.R
 import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
 import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
@@ -83,7 +83,6 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.compose.modifiers.thenIf
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.DurationUnit
 import kotlinx.coroutines.async
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
deleted file mode 100644
index 99fe26c..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.composable
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.gestures.detectVerticalDragGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.unit.IntSize
-import com.android.systemui.R
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
-import com.android.systemui.statusbar.ui.composable.StatusBar
-import com.android.systemui.util.time.SystemClock
-
-@Composable
-fun MultiShade(
-    viewModel: MultiShadeViewModel,
-    clock: SystemClock,
-    modifier: Modifier = Modifier,
-) {
-    val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
-    val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState()
-
-    // TODO(b/273298030): find a different way to get the height constraint from its parent.
-    BoxWithConstraints(modifier = modifier) {
-        val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }
-
-        Scrim(
-            modifier = Modifier.fillMaxSize(),
-            remoteTouch = viewModel::onScrimTouched,
-            alpha = { scrimAlpha },
-            isScrimEnabled = isScrimEnabled,
-        )
-        Shade(
-            viewModel = viewModel.leftShade,
-            currentTimeMillis = clock::elapsedRealtime,
-            containerHeightPx = maxHeightPx,
-            modifier = Modifier.align(Alignment.TopStart),
-        ) {
-            Column {
-                StatusBar()
-                Notifications()
-            }
-        }
-        Shade(
-            viewModel = viewModel.rightShade,
-            currentTimeMillis = clock::elapsedRealtime,
-            containerHeightPx = maxHeightPx,
-            modifier = Modifier.align(Alignment.TopEnd),
-        ) {
-            Column {
-                StatusBar()
-                QuickSettings()
-            }
-        }
-        Shade(
-            viewModel = viewModel.singleShade,
-            currentTimeMillis = clock::elapsedRealtime,
-            containerHeightPx = maxHeightPx,
-            modifier = Modifier,
-        ) {
-            Column {
-                StatusBar()
-                Notifications()
-                QuickSettings()
-            }
-        }
-    }
-}
-
-@Composable
-private fun Scrim(
-    remoteTouch: (ProxiedInputModel) -> Unit,
-    alpha: () -> Float,
-    isScrimEnabled: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    var size by remember { mutableStateOf(IntSize.Zero) }
-
-    Box(
-        modifier =
-            modifier
-                .graphicsLayer { this.alpha = alpha() }
-                .background(colorResource(R.color.opaque_scrim))
-                .fillMaxSize()
-                .onSizeChanged { size = it }
-                .then(
-                    if (isScrimEnabled) {
-                        Modifier.pointerInput(Unit) {
-                                detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
-                            }
-                            .pointerInput(Unit) {
-                                detectVerticalDragGestures(
-                                    onVerticalDrag = { change, dragAmount ->
-                                        remoteTouch(
-                                            ProxiedInputModel.OnDrag(
-                                                xFraction = change.position.x / size.width,
-                                                yDragAmountPx = dragAmount,
-                                            )
-                                        )
-                                    },
-                                    onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
-                                    onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
-                                )
-                            }
-                    } else {
-                        Modifier
-                    }
-                )
-    )
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
deleted file mode 100644
index cfcc2fb..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.composable
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.android.compose.modifiers.height
-import com.android.compose.modifiers.padding
-import com.android.compose.swipeable.FixedThreshold
-import com.android.compose.swipeable.SwipeableState
-import com.android.compose.swipeable.ThresholdConfig
-import com.android.compose.swipeable.rememberSwipeableState
-import com.android.compose.swipeable.swipeable
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlinx.coroutines.launch
-
-/**
- * Renders a shade (container and content).
- *
- * This should be allowed to grow to fill the width and height of its container.
- *
- * @param viewModel The view-model for this shade.
- * @param currentTimeMillis A provider for the current time, in milliseconds.
- * @param containerHeightPx The height of the container that this shade is being shown in, in
- *   pixels.
- * @param modifier The Modifier.
- * @param content The content of the shade.
- */
-@Composable
-fun Shade(
-    viewModel: ShadeViewModel,
-    currentTimeMillis: () -> Long,
-    containerHeightPx: Float,
-    modifier: Modifier = Modifier,
-    content: @Composable () -> Unit = {},
-) {
-    val isVisible: Boolean by viewModel.isVisible.collectAsState()
-    if (!isVisible) {
-        return
-    }
-
-    val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
-    ReportNonProxiedInput(viewModel, interactionSource)
-
-    val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
-    HandleForcedCollapse(viewModel, swipeableState)
-    HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
-    ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)
-
-    val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
-    val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
-    val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()
-
-    val width: ShadeViewModel.Size by viewModel.width.collectAsState()
-    val density = LocalDensity.current
-
-    val anchors: Map<Float, ShadeState> =
-        remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }
-
-    ShadeContent(
-        shadeHeightPx = { swipeableState.offset.value },
-        overstretch = { swipeableState.overflow.value / containerHeightPx },
-        isSwipingEnabled = isSwipingEnabled,
-        swipeableState = swipeableState,
-        interactionSource = interactionSource,
-        anchors = anchors,
-        thresholds = { _, to ->
-            swipeableThresholds(
-                to = to,
-                swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
-                swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
-            )
-        },
-        modifier = modifier.shadeWidth(width, density),
-        content = content,
-    )
-}
-
-/**
- * Draws the content of the shade.
- *
- * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
- *   fully collapsed.
- * @param overstretch Provider for the current amount of vertical "overstretch" that the shade
- *   should be rendered with. This is `0` or a positive number that is a percentage of the total
- *   height of the shade when fully expanded. A value of `0` means that the shade is not stretched
- *   at all.
- * @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
- * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
- *   addition to direct control (proxied user input in addition to non-proxied/direct user input).
- * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
- *   occurs; this is used to configure the [swipeable] modifier.
- * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
- *   another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
- *   user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
- * @param content The content to render inside the shade.
- * @param modifier The [Modifier].
- */
-@Composable
-private fun ShadeContent(
-    shadeHeightPx: () -> Float,
-    overstretch: () -> Float,
-    isSwipingEnabled: Boolean,
-    swipeableState: SwipeableState<ShadeState>,
-    interactionSource: MutableInteractionSource,
-    anchors: Map<Float, ShadeState>,
-    thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
-    modifier: Modifier = Modifier,
-    content: @Composable () -> Unit = {},
-) {
-    /**
-     * Returns a function that takes in [Density] and returns the current padding around the shade
-     * content.
-     */
-    fun padding(
-        shadeHeightPx: () -> Float,
-    ): Density.() -> Int {
-        return {
-            min(
-                12.dp.toPx().roundToInt(),
-                shadeHeightPx().roundToInt(),
-            )
-        }
-    }
-
-    Surface(
-        shape = RoundedCornerShape(32.dp),
-        modifier =
-            modifier
-                .fillMaxWidth()
-                .height { shadeHeightPx().roundToInt() }
-                .padding(
-                    horizontal = padding(shadeHeightPx),
-                    vertical = padding(shadeHeightPx),
-                )
-                .graphicsLayer {
-                    // Applies the vertical over-stretching of the shade content that may happen if
-                    // the user keep dragging down when the shade is already fully-expanded.
-                    transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
-                    this.scaleY = 1 + overstretch().coerceAtLeast(0f)
-                }
-                .swipeable(
-                    enabled = isSwipingEnabled,
-                    state = swipeableState,
-                    interactionSource = interactionSource,
-                    anchors = anchors,
-                    thresholds = thresholds,
-                    orientation = Orientation.Vertical,
-                ),
-        content = content,
-    )
-}
-
-/** Funnels current shade expansion values into the view-model. */
-@Composable
-private fun ReportShadeExpansion(
-    viewModel: ShadeViewModel,
-    swipeableState: SwipeableState<ShadeState>,
-    containerHeightPx: Float,
-) {
-    LaunchedEffect(swipeableState.offset, containerHeightPx) {
-        snapshotFlow { swipeableState.offset.value / containerHeightPx }
-            .collect { expansion -> viewModel.onExpansionChanged(expansion) }
-    }
-}
-
-/** Funnels drag gesture start and end events into the view-model. */
-@Composable
-private fun ReportNonProxiedInput(
-    viewModel: ShadeViewModel,
-    interactionSource: InteractionSource,
-) {
-    LaunchedEffect(interactionSource) {
-        interactionSource.interactions.collect {
-            when (it) {
-                is DragInteraction.Start -> {
-                    viewModel.onDragStarted()
-                }
-                is DragInteraction.Stop -> {
-                    viewModel.onDragEnded()
-                }
-            }
-        }
-    }
-}
-
-/** When told to force collapse, collapses the shade. */
-@Composable
-private fun HandleForcedCollapse(
-    viewModel: ShadeViewModel,
-    swipeableState: SwipeableState<ShadeState>,
-) {
-    LaunchedEffect(viewModel) {
-        viewModel.isForceCollapsed.collect {
-            launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
-        }
-    }
-}
-
-/**
- * Handles proxied input (input originating outside of the UI of the shade) by driving the
- * [SwipeableState] accordingly.
- */
-@Composable
-private fun HandleProxiedInput(
-    viewModel: ShadeViewModel,
-    swipeableState: SwipeableState<ShadeState>,
-    currentTimeMillis: () -> Long,
-) {
-    val velocityTracker: VelocityTracker = remember { VelocityTracker() }
-    LaunchedEffect(viewModel) {
-        viewModel.proxiedInput.collect {
-            when (it) {
-                is ProxiedInputModel.OnDrag -> {
-                    velocityTracker.addPosition(
-                        timeMillis = currentTimeMillis.invoke(),
-                        position = Offset(0f, it.yDragAmountPx),
-                    )
-                    swipeableState.performDrag(it.yDragAmountPx)
-                }
-                is ProxiedInputModel.OnDragEnd -> {
-                    launch {
-                        val velocity = velocityTracker.calculateVelocity().y
-                        velocityTracker.resetTracking()
-                        // We use a VelocityTracker to keep a record of how fast the pointer was
-                        // moving such that we know how far to fling the shade when the gesture
-                        // ends. Flinging the SwipeableState using performFling is required after
-                        // one or more calls to performDrag such that the swipeable settles into one
-                        // of the states. Without doing that, the shade would remain unmoving in an
-                        // in-between state on the screen.
-                        swipeableState.performFling(velocity)
-                    }
-                }
-                is ProxiedInputModel.OnDragCancel -> {
-                    launch {
-                        velocityTracker.resetTracking()
-                        swipeableState.animateTo(swipeableState.progress.from)
-                    }
-                }
-                else -> Unit
-            }
-        }
-    }
-}
-
-/**
- * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
- *
- * @param density The [Density] of the display.
- * @param wholePx The whole amount that the given [Float] is a fraction of.
- * @return The dp size that's a fraction of the whole amount.
- */
-private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
-    return with(density) { (this@fractionToDp * wholePx).toDp() }
-}
-
-private fun Modifier.shadeWidth(
-    size: ShadeViewModel.Size,
-    density: Density,
-): Modifier {
-    return then(
-        when (size) {
-            is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
-            is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
-        }
-    )
-}
-
-/** Returns the pixel positions for each of the supported shade states. */
-private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
-    return mapOf(
-        0f to ShadeState.FullyCollapsed,
-        containerHeightPx to ShadeState.FullyExpanded,
-    )
-}
-
-/**
- * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
- * actually completes the expansion or collapse after the user lifts their pointer.
- */
-private fun swipeableThresholds(
-    to: ShadeState,
-    swipeExpandThreshold: Dp,
-    swipeCollapseThreshold: Dp,
-): ThresholdConfig {
-    return FixedThreshold(
-        when (to) {
-            ShadeState.FullyExpanded -> swipeExpandThreshold
-            ShadeState.FullyCollapsed -> swipeCollapseThreshold
-        }
-    )
-}
-
-/** Enumerates the shade UI states for [SwipeableState]. */
-private enum class ShadeState {
-    FullyCollapsed,
-    FullyExpanded,
-}
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
new file mode 100644
index 0000000..40bab5e
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:state_enabled="false"
+          android:color="?androidprv:attr/materialColorPrimary"
+          android:alpha="0.30"/>
+    <item android:color="?androidprv:attr/materialColorPrimary"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
new file mode 100644
index 0000000..e76ad99
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:state_enabled="false"
+          android:color="?androidprv:attr/materialColorOnPrimary"
+          android:alpha="0.30"/>
+    <item android:color="?androidprv:attr/materialColorOnPrimary"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_icon_volume.xml b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
index fce4e00..85d608f 100644
--- a/packages/SystemUI/res/drawable/media_output_icon_volume.xml
+++ b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
@@ -3,7 +3,8 @@
     android:height="24dp"
     android:viewportWidth="24"
     android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
+    android:tint="?attr/colorControlNormal"
+    android:autoMirrored="true">
   <path
       android:fillColor="@color/media_dialog_item_main_content"
       android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.125,8.475 15.812,9.575Q16.5,10.675 16.5,12Q16.5,13.325 15.812,14.4Q15.125,15.475 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
diff --git a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
index b937937..a877900 100644
--- a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
+++ b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
@@ -17,9 +17,9 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
     <corners
-        android:bottomLeftRadius="28dp"
-        android:topLeftRadius="28dp"
-        android:bottomRightRadius="0dp"
-        android:topRightRadius="0dp"/>
+        android:bottomLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+        android:topLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+        android:bottomRightRadius="@dimen/media_output_dialog_icon_right_radius"
+        android:topRightRadius="@dimen/media_output_dialog_icon_right_radius"/>
     <solid android:color="@color/media_dialog_item_background" />
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
index c4e45bf..9bc8b53 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
@@ -15,7 +15,6 @@
   ~ limitations under the License.
   -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
        android:insetTop="@dimen/dialog_button_vertical_inset"
        android:insetBottom="@dimen/dialog_button_vertical_inset">
     <ripple android:color="?android:attr/colorControlHighlight">
@@ -28,7 +27,7 @@
         <item>
             <shape android:shape="rectangle">
                 <corners android:radius="?android:attr/buttonCornerRadius"/>
-                <solid android:color="?androidprv:attr/materialColorPrimary"/>
+                <solid android:color="@color/qs_dialog_btn_filled_background"/>
                 <padding android:left="@dimen/dialog_button_horizontal_padding"
                          android:top="@dimen/dialog_button_vertical_padding"
                          android:right="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000..0d99b61
--- /dev/null
+++ b/packages/SystemUI/res/values-ldrtl/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+    <dimen name="media_output_dialog_icon_left_radius">0dp</dimen>
+    <dimen name="media_output_dialog_icon_right_radius">28dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3366f4f..0e88c31 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1346,6 +1346,8 @@
     <dimen name="media_output_dialog_default_margin_end">16dp</dimen>
     <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen>
     <dimen name="media_output_dialog_list_padding_top">8dp</dimen>
+    <dimen name="media_output_dialog_icon_left_radius">28dp</dimen>
+    <dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
 
     <!-- Distance that the full shade transition takes in order to complete by tapping on a button
          like "expand". -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f8c13b0..c306a79 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2582,6 +2582,9 @@
     <!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] -->
     <string name="controls_structure_tooltip">Swipe to see more</string>
 
+    <!-- Accessibility action informing the user how they can retry face authentication [CHAR LIMIT=NONE] -->
+    <string name="retry_face">Retry face authentication</string>
+
     <!-- Message to tell the user to wait while systemui attempts to load a set of
          recommended controls [CHAR_LIMIT=60] -->
     <string name="controls_seeding_in_progress">Loading recommendations</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 31f40e9..1c46a91 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1116,7 +1116,7 @@
     <style name="Widget.Dialog.Button">
         <item name="android:buttonCornerRadius">28dp</item>
         <item name="android:background">@drawable/qs_dialog_btn_filled</item>
-        <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+        <item name="android:textColor">@color/qs_dialog_btn_filled_text_color</item>
         <item name="android:textSize">14sp</item>
         <item name="android:lineHeight">20sp</item>
         <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index d8085b9..22cdb30 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -20,6 +20,7 @@
 import android.os.PowerManager
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.keyguard.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION
 import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
 import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED
 import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED
@@ -71,6 +72,7 @@
     NOTIFICATION_PANEL_CLICKED,
     QS_EXPANDED,
     PICK_UP_GESTURE_TRIGGERED,
+    ACCESSIBILITY_ACTION,
 )
 annotation class FaceAuthApiRequestReason {
     companion object {
@@ -80,6 +82,7 @@
         const val QS_EXPANDED = "Face auth due to QS expansion."
         const val PICK_UP_GESTURE_TRIGGERED =
             "Face auth due to pickup gesture triggered when the device is awake and not from AOD."
+        const val ACCESSIBILITY_ACTION = "Face auth due to an accessibility action."
     }
 }
 
@@ -217,7 +220,8 @@
     @UiEvent(doc = STRONG_AUTH_ALLOWED_CHANGED)
     FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED(1255, STRONG_AUTH_ALLOWED_CHANGED),
     @UiEvent(doc = NON_STRONG_BIOMETRIC_ALLOWED_CHANGED)
-    FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED);
+    FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED),
+    @UiEvent(doc = ACCESSIBILITY_ACTION) FACE_AUTH_ACCESSIBILITY_ACTION(1454, ACCESSIBILITY_ACTION);
 
     override fun getId(): Int = this.id
 
@@ -233,6 +237,8 @@
             FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED,
         QS_EXPANDED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED,
         PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+        PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+        ACCESSIBILITY_ACTION to FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION,
     )
 
 /** Converts the [reason] to the corresponding [FaceAuthUiEvent]. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index f952337..bc24249 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -68,6 +68,7 @@
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.biometrics.SideFpsUiRequestSource;
 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
@@ -417,9 +418,11 @@
             BouncerMessageInteractor bouncerMessageInteractor,
             Provider<JavaAdapter> javaAdapter,
             UserInteractor userInteractor,
+            FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
             Provider<SceneInteractor> sceneInteractor
     ) {
         super(view);
+        view.setAccessibilityDelegate(faceAuthAccessibilityDelegate);
         mLockPatternUtils = lockPatternUtils;
         mUpdateMonitor = keyguardUpdateMonitor;
         mSecurityModel = keyguardSecurityModel;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 6bd9df0..5833d98 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3158,6 +3158,10 @@
             return false;
         }
 
+        if (isFaceAuthInteractorEnabled()) {
+            return mFaceAuthInteractor.canFaceAuthRun();
+        }
+
         final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
         final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive
                 && !statusBarShadeLocked;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 2f6a68c..03ad132 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -54,6 +54,7 @@
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -188,6 +189,7 @@
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
     private final NotificationShadeWindowController mNotificationShadeController;
     private final ShadeController mShadeController;
+    private final Lazy<ShadeViewController> mShadeViewController;
     private final StatusBarWindowCallback mNotificationShadeCallback;
     private boolean mDismissNotificationShadeActionRegistered;
 
@@ -196,12 +198,14 @@
             UserTracker userTracker,
             NotificationShadeWindowController notificationShadeController,
             ShadeController shadeController,
+            Lazy<ShadeViewController> shadeViewController,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
             Optional<Recents> recentsOptional,
             DisplayTracker displayTracker) {
         mContext = context;
         mUserTracker = userTracker;
         mShadeController = shadeController;
+        mShadeViewController = shadeViewController;
         mRecentsOptional = recentsOptional;
         mDisplayTracker = displayTracker;
         mReceiver = new SystemActionsBroadcastReceiver();
@@ -330,8 +334,7 @@
         final Optional<CentralSurfaces> centralSurfacesOptional =
                 mCentralSurfacesOptionalLazy.get();
         if (centralSurfacesOptional.isPresent()
-                && centralSurfacesOptional.get().getShadeViewController() != null
-                && centralSurfacesOptional.get().getShadeViewController().isPanelExpanded()
+                && mShadeViewController.get().isPanelExpanded()
                 && !centralSurfacesOptional.get().isKeyguardShowing()) {
             if (!mDismissNotificationShadeActionRegistered) {
                 mA11yManager.registerSystemAction(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
index 783460c..0ef256d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
@@ -145,6 +145,8 @@
      */
     @MainThread
     fun updateFontScaleDelayed(delayMsFromSource: Long) {
+        doneButton.isEnabled = false
+
         var delayMs = delayMsFromSource
         if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) {
             delayMs += MIN_UPDATE_INTERVAL_MS
@@ -197,17 +199,22 @@
             title.post {
                 title.setTextAppearance(R.style.TextAppearance_Dialog_Title)
                 doneButton.setTextAppearance(R.style.Widget_Dialog_Button)
+                doneButton.isEnabled = true
             }
         }
     }
 
     @WorkerThread
     fun updateFontScale() {
-        systemSettings.putStringForUser(
-            Settings.System.FONT_SCALE,
-            strEntryValues[lastProgress.get()],
-            userTracker.userId
-        )
+        if (
+            !systemSettings.putStringForUser(
+                Settings.System.FONT_SCALE,
+                strEntryValues[lastProgress.get()],
+                userTracker.userId
+            )
+        ) {
+            title.post { doneButton.isEnabled = true }
+        }
     }
 
     @WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index a977966..deb3d03 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
 package com.android.systemui.authentication.data.repository
 
 import com.android.internal.widget.LockPatternChecker
@@ -29,6 +27,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.time.SystemClock
 import dagger.Binds
 import dagger.Module
@@ -38,16 +37,14 @@
 import kotlin.coroutines.suspendCoroutine
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 /** Defines interface for classes that can access authentication-related application state. */
@@ -156,32 +153,18 @@
     }
 
     override val isAutoConfirmEnabled: StateFlow<Boolean> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .flatMapLatest { userId ->
-                flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) }
-                    .flowOn(backgroundDispatcher)
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
+        refreshingFlow(
+            initialValue = false,
+            getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled,
+        )
 
     override val hintedPinLength: Int = 6
 
     override val isPatternVisible: StateFlow<Boolean> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .flatMapLatest { userId ->
-                flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) }
-                    .flowOn(backgroundDispatcher)
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = true,
-            )
+        refreshingFlow(
+            initialValue = true,
+            getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
+        )
 
     private val _throttling = MutableStateFlow(AuthenticationThrottlingModel())
     override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow()
@@ -276,6 +259,48 @@
             )
         }
     }
+
+    /**
+     * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is
+     * invoked on a background thread every time the selected user is changed and every time a new
+     * downstream subscriber is added to the flow.
+     *
+     * Initially, the flow will emit [initialValue] while it refreshes itself in the background by
+     * invoking the [getFreshValue] function and emitting the fresh value when that's done.
+     *
+     * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the
+     * new value.
+     *
+     * Every time a new downstream subscriber is added to the flow it first receives the latest
+     * cached value that's either the [initialValue] or the latest previously fetched value. In
+     * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a
+     * subsequent emission of that newest value.
+     */
+    private fun <T> refreshingFlow(
+        initialValue: T,
+        getFreshValue: suspend (selectedUserId: Int) -> T,
+    ): StateFlow<T> {
+        val flow = MutableStateFlow(initialValue)
+        applicationScope.launch {
+            combine(
+                    // Emits a value initially and every time the selected user is changed.
+                    userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
+                    // Emits a value only when the number of downstream subscribers of this flow
+                    // increases.
+                    flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
+                        ->
+                        current > previous
+                    },
+                ) { selectedUserId, _ ->
+                    selectedUserId
+                }
+                .collect { selectedUserId ->
+                    flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) }
+                }
+        }
+
+        return flow.asStateFlow()
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index b482977..d4371bf 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -104,7 +104,9 @@
             }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.Eagerly,
+                // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+                // downstream continues to receive old/stale/cached values.
+                started = SharingStarted.WhileSubscribed(),
                 initialValue = null,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
new file mode 100644
index 0000000..b9fa240
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import javax.inject.Inject
+
+/**
+ * Accessibility delegate that will add a click accessibility action to a view when face auth can
+ * run. When the click a11y action is triggered, face auth will retry.
+ */
+@SysUISingleton
+class FaceAuthAccessibilityDelegate
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val faceAuthInteractor: KeyguardFaceAuthInteractor,
+) : View.AccessibilityDelegate() {
+    override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+        super.onInitializeAccessibilityNodeInfo(host, info)
+        if (keyguardUpdateMonitor.shouldListenForFace()) {
+            val clickActionToRetryFace =
+                AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+                    resources.getString(R.string.retry_face)
+                )
+            info.addAction(clickActionToRetryFace)
+        }
+    }
+
+    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+        return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
+            keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)
+            faceAuthInteractor.onAccessibilityAction()
+            true
+        } else super.performAccessibilityAction(host, action, args)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 083e21f..37ce444 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -17,7 +17,12 @@
 
 import android.app.ActivityTaskManager
 import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
 import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
 import android.hardware.biometrics.BiometricOverlayConstants
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@
 import android.hardware.fingerprint.ISidefpsController
 import android.os.Handler
 import android.util.Log
+import android.util.RotationUtils
 import android.view.Display
 import android.view.DisplayInfo
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.Surface
 import android.view.View
+import android.view.View.AccessibilityDelegate
 import android.view.ViewPropertyAnimator
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RawRes
 import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -55,7 +64,6 @@
 import com.android.systemui.util.traceSection
 import java.io.PrintWriter
 import javax.inject.Inject
-import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -78,7 +86,6 @@
     @Main private val mainExecutor: DelayableExecutor,
     @Main private val handler: Handler,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
-    private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
     @Application private val scope: CoroutineScope,
     dumpManager: DumpManager
 ) : Dumpable {
@@ -243,15 +250,105 @@
     private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
         val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
         overlayView = view
-        SideFpsOverlayViewBinder.bind(
-            view = view,
-            viewModel = sideFpsOverlayViewModelFactory.get(),
-            overlayViewParams = overlayViewParams,
-            reason = reason,
-            context = context,
+        val display = context.display!!
+        // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
+        display.getDisplayInfo(displayInfo)
+        val offsets =
+            sensorProps.getLocation(display.uniqueId).let { location ->
+                if (location == null) {
+                    Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+                }
+                location ?: sensorProps.location
+            }
+        overlayOffsets = offsets
+
+        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+        view.rotation =
+            display.asSideFpsAnimationRotation(
+                offsets.isYAligned(),
+                getRotationFromDefault(displayInfo.rotation)
+            )
+        lottie.setAnimation(
+            display.asSideFpsAnimation(
+                offsets.isYAligned(),
+                getRotationFromDefault(displayInfo.rotation)
+            )
         )
+        lottie.addLottieOnCompositionLoadedListener {
+            // Check that view is not stale, and that overlayView has not been hidden/removed
+            if (overlayView != null && overlayView == view) {
+                updateOverlayParams(display, it.bounds)
+            }
+        }
         orientationReasonListener.reason = reason
+        lottie.addOverlayDynamicColor(context, reason)
+
+        /**
+         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
+         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
+         * in focus
+         */
+        view.setAccessibilityDelegate(
+            object : AccessibilityDelegate() {
+                override fun dispatchPopulateAccessibilityEvent(
+                    host: View,
+                    event: AccessibilityEvent
+                ): Boolean {
+                    return if (
+                        event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+                    ) {
+                        true
+                    } else {
+                        super.dispatchPopulateAccessibilityEvent(host, event)
+                    }
+                }
+            }
+        )
     }
+
+    @VisibleForTesting
+    fun updateOverlayParams(display: Display, bounds: Rect) {
+        val isNaturalOrientation = display.isNaturalOrientation()
+        val isDefaultOrientation =
+            if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
+        val size = windowManager.maximumWindowMetrics.bounds
+
+        val displayWidth = if (isDefaultOrientation) size.width() else size.height()
+        val displayHeight = if (isDefaultOrientation) size.height() else size.width()
+        val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
+        val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
+        val sensorBounds =
+            if (overlayOffsets.isYAligned()) {
+                Rect(
+                    displayWidth - boundsWidth,
+                    overlayOffsets.sensorLocationY,
+                    displayWidth,
+                    overlayOffsets.sensorLocationY + boundsHeight
+                )
+            } else {
+                Rect(
+                    overlayOffsets.sensorLocationX,
+                    0,
+                    overlayOffsets.sensorLocationX + boundsWidth,
+                    boundsHeight
+                )
+            }
+
+        RotationUtils.rotateBounds(
+            sensorBounds,
+            Rect(0, 0, displayWidth, displayHeight),
+            getRotationFromDefault(display.rotation)
+        )
+
+        overlayViewParams.x = sensorBounds.left
+        overlayViewParams.y = sensorBounds.top
+
+        windowManager.updateViewLayout(overlayView, overlayViewParams)
+    }
+
+    private fun getRotationFromDefault(rotation: Int): Int =
+        if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
 }
 
 private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -276,12 +373,89 @@
 private fun ActivityTaskManager.topClass(): String =
     getTasks(1).firstOrNull()?.topActivity?.className ?: ""
 
+@RawRes
+private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
+    when (rotationFromDefault) {
+        Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+        Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+        else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
+    }
+
+private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
+    when (rotationFromDefault) {
+        Surface.ROTATION_90 -> if (yAligned) 0f else 180f
+        Surface.ROTATION_180 -> 180f
+        Surface.ROTATION_270 -> if (yAligned) 180f else 0f
+        else -> 0f
+    }
+
 private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
 
 private fun Display.isNaturalOrientation(): Boolean =
     rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
 
-public class OrientationReasonListener(
+private fun LottieAnimationView.addOverlayDynamicColor(
+    context: Context,
+    @BiometricOverlayConstants.ShowReason reason: Int
+) {
+    fun update() {
+        val isKeyguard = reason == REASON_AUTH_KEYGUARD
+        if (isKeyguard) {
+            val color =
+                com.android.settingslib.Utils.getColorAttrDefaultColor(
+                    context,
+                    com.android.internal.R.attr.materialColorPrimaryFixed
+                )
+            val outerRimColor =
+                com.android.settingslib.Utils.getColorAttrDefaultColor(
+                    context,
+                    com.android.internal.R.attr.materialColorPrimaryFixedDim
+                )
+            val chevronFill =
+                com.android.settingslib.Utils.getColorAttrDefaultColor(
+                    context,
+                    com.android.internal.R.attr.materialColorOnPrimaryFixed
+                )
+            addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+            }
+            addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
+            }
+            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+            }
+        } else {
+            if (!isDarkMode(context)) {
+                addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+                    PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
+                }
+            }
+            for (key in listOf(".blue600", ".blue400")) {
+                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+                    PorterDuffColorFilter(
+                        context.getColor(R.color.settingslib_color_blue400),
+                        PorterDuff.Mode.SRC_ATOP
+                    )
+                }
+            }
+        }
+    }
+
+    if (composition != null) {
+        update()
+    } else {
+        addLottieOnCompositionLoadedListener { update() }
+    }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+    return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
+
+@VisibleForTesting
+class OrientationReasonListener(
     context: Context,
     displayManager: DisplayManager,
     handler: Handler,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index efbde4c..efc92ad 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.biometrics.data.repository
 
-import android.hardware.biometrics.ComponentInfoInternal
 import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
 import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -33,8 +30,10 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.shareIn
 
 /**
@@ -44,17 +43,22 @@
  */
 interface FingerprintPropertyRepository {
 
+    /**
+     * If the repository is initialized or not. Other properties are defaults until this is true.
+     */
+    val isInitialized: Flow<Boolean>
+
     /** The id of fingerprint sensor. */
-    val sensorId: Flow<Int>
+    val sensorId: StateFlow<Int>
 
     /** The security strength of sensor (convenience, weak, strong). */
-    val strength: Flow<SensorStrength>
+    val strength: StateFlow<SensorStrength>
 
     /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
-    val sensorType: Flow<FingerprintSensorType>
+    val sensorType: StateFlow<FingerprintSensorType>
 
     /** The sensor location relative to each physical display. */
-    val sensorLocations: Flow<Map<String, SensorLocationInternal>>
+    val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
 }
 
 @SysUISingleton
@@ -62,10 +66,10 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    private val fingerprintManager: FingerprintManager?
+    private val fingerprintManager: FingerprintManager?,
 ) : FingerprintPropertyRepository {
 
-    private val props: Flow<FingerprintSensorPropertiesInternal> =
+    override val isInitialized: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
                     object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,47 +77,45 @@
                             sensors: List<FingerprintSensorPropertiesInternal>
                         ) {
                             if (sensors.isNotEmpty()) {
-                                trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
-                            } else {
-                                trySendWithFailureLogging(
-                                    DEFAULT_PROPS,
-                                    TAG,
-                                    "initialize with default properties"
-                                )
+                                setProperties(sensors[0])
+                                trySendWithFailureLogging(true, TAG, "initialize properties")
                             }
                         }
                     }
                 fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
-                trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
+                trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
                 awaitClose {}
             }
             .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
 
-    override val sensorId: Flow<Int> = props.map { it.sensorId }
-    override val strength: Flow<SensorStrength> =
-        props.map { sensorStrengthIntToObject(it.sensorStrength) }
-    override val sensorType: Flow<FingerprintSensorType> =
-        props.map { sensorTypeIntToObject(it.sensorType) }
-    override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
-        props.map {
-            it.allLocations.associateBy { sensorLocationInternal ->
+    private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
+    override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+
+    private val _strength: MutableStateFlow<SensorStrength> =
+        MutableStateFlow(SensorStrength.CONVENIENCE)
+    override val strength = _strength.asStateFlow()
+
+    private val _sensorType: MutableStateFlow<FingerprintSensorType> =
+        MutableStateFlow(FingerprintSensorType.UNKNOWN)
+    override val sensorType = _sensorType.asStateFlow()
+
+    private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
+        MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
+    override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+        _sensorLocations.asStateFlow()
+
+    private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
+        _sensorId.value = prop.sensorId
+        _strength.value = sensorStrengthIntToObject(prop.sensorStrength)
+        _sensorType.value = sensorTypeIntToObject(prop.sensorType)
+        _sensorLocations.value =
+            prop.allLocations.associateBy { sensorLocationInternal ->
                 sensorLocationInternal.displayId
             }
-        }
+    }
 
     companion object {
         private const val TAG = "FingerprintPropertyRepositoryImpl"
-        private val DEFAULT_PROPS =
-            FingerprintSensorPropertiesInternal(
-                -1 /* sensorId */,
-                SensorProperties.STRENGTH_CONVENIENCE,
-                0 /* maxEnrollmentsPerUser */,
-                listOf<ComponentInfoInternal>(),
-                FingerprintSensorProperties.TYPE_UNKNOWN,
-                false /* halControlsIllumination */,
-                true /* resetLockoutRequiresHardwareAuthToken */,
-                listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
-            )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
index 37f39cb..aa85e5f3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
@@ -17,24 +17,16 @@
 package com.android.systemui.biometrics.domain.interactor
 
 import android.hardware.biometrics.SensorLocationInternal
+import android.util.Log
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
 
 /** Business logic for SideFps overlay offsets. */
 interface SideFpsOverlayInteractor {
-    /** The displayId of the current display. */
-    val displayId: Flow<String>
 
-    /** The corresponding offsets based on different displayId. */
-    val overlayOffsets: Flow<SensorLocationInternal>
-
-    /** Update the displayId. */
-    fun changeDisplay(displayId: String?)
+    /** Get the corresponding offsets based on different displayId. */
+    fun getOverlayOffsets(displayId: String): SensorLocationInternal
 }
 
 @SysUISingleton
@@ -43,16 +35,14 @@
 constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
     SideFpsOverlayInteractor {
 
-    private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
-    override val displayId: Flow<String> = _displayId.asStateFlow()
-
-    override val overlayOffsets: Flow<SensorLocationInternal> =
-        combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
-            offsets[displayId] ?: SensorLocationInternal.DEFAULT
+    override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
+        val offsets = fingerprintPropertyRepository.sensorLocations.value
+        return if (offsets.containsKey(displayId)) {
+            offsets[displayId]!!
+        } else {
+            Log.w(TAG, "No location specified for display: $displayId")
+            offsets[""]!!
         }
-
-    override fun changeDisplay(displayId: String?) {
-        _displayId.value = displayId ?: ""
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
deleted file mode 100644
index 0409519..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.binder
-
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Color
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import android.hardware.biometrics.BiometricOverlayConstants
-import android.view.View
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityEvent
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.R
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.launch
-
-/** Sub-binder for SideFpsOverlayView. */
-object SideFpsOverlayViewBinder {
-
-    /** Bind the view. */
-    @JvmStatic
-    fun bind(
-        view: View,
-        viewModel: SideFpsOverlayViewModel,
-        overlayViewParams: WindowManager.LayoutParams,
-        @BiometricOverlayConstants.ShowReason reason: Int,
-        @Application context: Context
-    ) {
-        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
-        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
-
-        viewModel.changeDisplay()
-
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.sideFpsAnimationRotation.collect { rotation ->
-                        view.rotation = rotation
-                    }
-                }
-
-                launch {
-                    // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation
-                    // in order to add scuba tests in the future.
-                    viewModel.sideFpsAnimation.collect { animation ->
-                        lottie.setAnimation(animation)
-                    }
-                }
-
-                launch {
-                    viewModel.sensorBounds.collect { sensorBounds ->
-                        overlayViewParams.x = sensorBounds.left
-                        overlayViewParams.y = sensorBounds.top
-
-                        windowManager.updateViewLayout(view, overlayViewParams)
-                    }
-                }
-
-                launch {
-                    viewModel.overlayOffsets.collect { overlayOffsets ->
-                        lottie.addLottieOnCompositionLoadedListener {
-                            viewModel.updateSensorBounds(
-                                it.bounds,
-                                windowManager.maximumWindowMetrics.bounds,
-                                overlayOffsets
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        lottie.addOverlayDynamicColor(context, reason)
-
-        /**
-         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
-         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
-         * in focus
-         */
-        view.accessibilityDelegate =
-            object : View.AccessibilityDelegate() {
-                override fun dispatchPopulateAccessibilityEvent(
-                    host: View,
-                    event: AccessibilityEvent
-                ): Boolean {
-                    return if (
-                        event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
-                    ) {
-                        true
-                    } else {
-                        super.dispatchPopulateAccessibilityEvent(host, event)
-                    }
-                }
-            }
-    }
-}
-
-private fun LottieAnimationView.addOverlayDynamicColor(
-    context: Context,
-    @BiometricOverlayConstants.ShowReason reason: Int
-) {
-    fun update() {
-        val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-        if (isKeyguard) {
-            val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color
-            val chevronFill =
-                com.android.settingslib.Utils.getColorAttrDefaultColor(
-                    context,
-                    android.R.attr.textColorPrimaryInverse
-                )
-            for (key in listOf(".blue600", ".blue400")) {
-                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
-                    PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
-                }
-            }
-            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
-                PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
-            }
-        } else if (!isDarkMode(context)) {
-            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
-                PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
-            }
-        } else if (isDarkMode(context)) {
-            for (key in listOf(".blue600", ".blue400")) {
-                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
-                    PorterDuffColorFilter(
-                        context.getColor(R.color.settingslib_color_blue400),
-                        PorterDuff.Mode.SRC_ATOP
-                    )
-                }
-            }
-        }
-    }
-
-    if (composition != null) {
-        update()
-    } else {
-        addLottieOnCompositionLoadedListener { update() }
-    }
-}
-
-private fun isDarkMode(context: Context): Boolean {
-    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
-    return darkMode == Configuration.UI_MODE_NIGHT_YES
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
deleted file mode 100644
index e938b4e..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.viewmodel
-
-import android.content.Context
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.util.RotationUtils
-import android.view.Display
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.annotation.RawRes
-import com.android.systemui.R
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.dagger.qualifiers.Application
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-
-/** View-model for SideFpsOverlayView. */
-class SideFpsOverlayViewModel
-@Inject
-constructor(
-    @Application private val context: Context,
-    private val sideFpsOverlayInteractor: SideFpsOverlayInteractor,
-) {
-
-    private val isReverseDefaultRotation =
-        context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
-
-    private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect())
-    val sensorBounds = _sensorBounds.asStateFlow()
-
-    val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets
-
-    /** Update the displayId. */
-    fun changeDisplay() {
-        sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId)
-    }
-
-    /** Determine the rotation of the sideFps animation given the overlay offsets. */
-    val sideFpsAnimationRotation: Flow<Float> =
-        overlayOffsets.map { overlayOffsets ->
-            val display = context.display!!
-            val displayInfo: DisplayInfo = DisplayInfo()
-            // b/284098873 `context.display.rotation` may not up-to-date, we use
-            // displayInfo.rotation
-            display.getDisplayInfo(displayInfo)
-            val yAligned: Boolean = overlayOffsets.isYAligned()
-            when (getRotationFromDefault(displayInfo.rotation)) {
-                Surface.ROTATION_90 -> if (yAligned) 0f else 180f
-                Surface.ROTATION_180 -> 180f
-                Surface.ROTATION_270 -> if (yAligned) 180f else 0f
-                else -> 0f
-            }
-        }
-
-    /** Populate the sideFps animation from the overlay offsets. */
-    @RawRes
-    val sideFpsAnimation: Flow<Int> =
-        overlayOffsets.map { overlayOffsets ->
-            val display = context.display!!
-            val displayInfo: DisplayInfo = DisplayInfo()
-            // b/284098873 `context.display.rotation` may not up-to-date, we use
-            // displayInfo.rotation
-            display.getDisplayInfo(displayInfo)
-            val yAligned: Boolean = overlayOffsets.isYAligned()
-            when (getRotationFromDefault(displayInfo.rotation)) {
-                Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
-                Surface.ROTATION_180 ->
-                    if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
-                else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
-            }
-        }
-
-    /**
-     * Calculate and update the bounds of the sensor based on the bounds of the overlay view, the
-     * maximum bounds of the window, and the offsets of the sensor location.
-     */
-    fun updateSensorBounds(
-        bounds: Rect,
-        maximumWindowBounds: Rect,
-        offsets: SensorLocationInternal
-    ) {
-        val isNaturalOrientation = context.display!!.isNaturalOrientation()
-        val isDefaultOrientation =
-            if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
-
-        val displayWidth =
-            if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height()
-        val displayHeight =
-            if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width()
-        val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
-        val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
-
-        val sensorBounds =
-            if (offsets.isYAligned()) {
-                Rect(
-                    displayWidth - boundsWidth,
-                    offsets.sensorLocationY,
-                    displayWidth,
-                    offsets.sensorLocationY + boundsHeight
-                )
-            } else {
-                Rect(
-                    offsets.sensorLocationX,
-                    0,
-                    offsets.sensorLocationX + boundsWidth,
-                    boundsHeight
-                )
-            }
-
-        val displayInfo: DisplayInfo = DisplayInfo()
-        context.display!!.getDisplayInfo(displayInfo)
-
-        RotationUtils.rotateBounds(
-            sensorBounds,
-            Rect(0, 0, displayWidth, displayHeight),
-            getRotationFromDefault(displayInfo.rotation)
-        )
-
-        _sensorBounds.value = sensorBounds
-    }
-
-    private fun getRotationFromDefault(rotation: Int): Int =
-        if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
-}
-
-private fun Display.isNaturalOrientation(): Boolean =
-    rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
-private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 1b14acc..38fb8b9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -60,7 +60,9 @@
             }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.Eagerly,
+                // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+                // downstream continues to receive old/stale/cached values.
+                started = SharingStarted.WhileSubscribed(),
                 initialValue = ActionButtonAppearance.Hidden,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index b15c60e..85f31e5 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -21,13 +21,11 @@
 import android.view.View
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
 
 /**
  * A facade to interact with Compose, when it is available.
@@ -64,13 +62,6 @@
     ): View
 
     /** Create a [View] to represent [viewModel] on screen. */
-    fun createMultiShadeView(
-        context: Context,
-        viewModel: MultiShadeViewModel,
-        clock: SystemClock,
-    ): View
-
-    /** Create a [View] to represent [viewModel] on screen. */
     fun createSceneContainerView(
         context: Context,
         viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
index 99451f2..6f05e83 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
@@ -37,12 +37,15 @@
  */
 public class ShadeTouchHandler implements DreamTouchHandler {
     private final Optional<CentralSurfaces> mSurfaces;
+    private final ShadeViewController mShadeViewController;
     private final int mInitiationHeight;
 
     @Inject
     ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
+            ShadeViewController shadeViewController,
             @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
         mSurfaces = centralSurfaces;
+        mShadeViewController = shadeViewController;
         mInitiationHeight = initiationHeight;
     }
 
@@ -54,12 +57,7 @@
         }
 
         session.registerInputListener(ev -> {
-            final ShadeViewController viewController =
-                    mSurfaces.map(CentralSurfaces::getShadeViewController).orElse(null);
-
-            if (viewController != null) {
-                viewController.handleExternalTouch((MotionEvent) ev);
-            }
+            mShadeViewController.handleExternalTouch((MotionEvent) ev);
 
             if (ev instanceof MotionEvent) {
                 if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index efa5981..e6820a3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -77,7 +77,7 @@
     // TODO(b/278873737): Tracking Bug
     @JvmField
     val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE =
-            releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
+        releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
 
     // TODO(b/277338665): Tracking Bug
     @JvmField
@@ -92,11 +92,7 @@
     // TODO(b/288326013): Tracking Bug
     @JvmField
     val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
-            unreleasedFlag(
-                    288326013,
-                    "notification_async_hybrid_view_inflation",
-                    teamfood = false
-            )
+        unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false)
 
     @JvmField
     val ANIMATED_NOTIFICATION_SHADE_INSETS =
@@ -104,18 +100,17 @@
 
     // TODO(b/268005230): Tracking Bug
     @JvmField
-    val SENSITIVE_REVEAL_ANIM =
-        unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
+    val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
 
     // TODO(b/280783617): Tracking Bug
     @Keep
     @JvmField
     val BUILDER_EXTRAS_OVERRIDE =
-            sysPropBooleanFlag(
-                    128,
-                    "persist.sysui.notification.builder_extras_override",
-                    default = false
-            )
+        sysPropBooleanFlag(
+            128,
+            "persist.sysui.notification.builder_extras_override",
+            default = false
+        )
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
@@ -133,16 +128,17 @@
 
     // TODO(b/254512676): Tracking Bug
     @JvmField
-    val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(
-        207,
-        R.bool.config_enableLockScreenCustomClocks,
-        "lockscreen_custom_clocks"
-    )
+    val LOCKSCREEN_CUSTOM_CLOCKS =
+        resourceBooleanFlag(
+            207,
+            R.bool.config_enableLockScreenCustomClocks,
+            "lockscreen_custom_clocks"
+        )
 
     // TODO(b/275694445): Tracking Bug
     @JvmField
-    val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208,
-        "lockscreen_without_secure_lock_when_dreaming")
+    val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING =
+        releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming")
 
     // TODO(b/286092087): Tracking Bug
     @JvmField
@@ -156,8 +152,7 @@
      * 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 = releasedFlag(212, "step_clock_animation")
+    @JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
 
     /**
      * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -183,13 +178,11 @@
     @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
 
     // TODO(b/262780002): Tracking Bug
-    @JvmField
-    val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
+    @JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
 
     // flag for controlling auto pin confirmation and material u shapes in bouncer
     @JvmField
-    val AUTO_PIN_CONFIRMATION =
-        releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
+    val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
 
     // TODO(b/262859270): Tracking Bug
     @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
@@ -206,20 +199,11 @@
     /** Whether the long-press gesture to open wallpaper picker is enabled. */
     // TODO(b/266242192): Tracking Bug
     @JvmField
-    val LOCK_SCREEN_LONG_PRESS_ENABLED =
-        releasedFlag(
-            228,
-            "lock_screen_long_press_enabled"
-        )
+    val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled")
 
     /** Enables UI updates for AI wallpapers in the wallpaper picker. */
     // TODO(b/267722622): Tracking Bug
-    @JvmField
-    val WALLPAPER_PICKER_UI_FOR_AIWP =
-            releasedFlag(
-                    229,
-                    "wallpaper_picker_ui_for_aiwp"
-            )
+    @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp")
 
     /** Whether to use a new data source for intents to run on keyguard dismissal. */
     // TODO(b/275069969): Tracking bug.
@@ -239,27 +223,21 @@
 
     /** Provide new auth messages on the bouncer. */
     // TODO(b/277961132): Tracking bug.
-    @JvmField
-    val REVAMPED_BOUNCER_MESSAGES =
-        unreleasedFlag(234, "revamped_bouncer_messages")
+    @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages")
 
     /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
     // TODO(b/279794160): Tracking bug.
-    @JvmField
-    val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
-
+    @JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
 
     /** Keyguard Migration */
 
     /** Migrate the indication area to the new keyguard root view. */
     // TODO(b/280067944): Tracking bug.
-    @JvmField
-    val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
+    @JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
 
     /**
-     * Migrate the bottom area to the new keyguard root view.
-     * Because there is no such thing as a "bottom area" after this, this also breaks it up into
-     * many smaller, modular pieces.
+     * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
+     * "bottom area" after this, this also breaks it up into many smaller, modular pieces.
      */
     // TODO(b/290652751): Tracking bug.
     @JvmField
@@ -268,36 +246,29 @@
 
     /** Whether to listen for fingerprint authentication over keyguard occluding activities. */
     // TODO(b/283260512): Tracking bug.
-    @JvmField
-    val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
+    @JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
 
     /** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
     // TODO(b/286563884): Tracking bug
-    @JvmField
-    val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
+    @JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
 
     // TODO(b/287268101): Tracking bug.
-    @JvmField
-    val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
+    @JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
 
     /** Migrate the lock icon view to the new keyguard root view. */
     // TODO(b/286552209): Tracking bug.
-    @JvmField
-    val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
+    @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
 
     // TODO(b/288276738): Tracking bug.
-    @JvmField
-    val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
+    @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
 
     /** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
     // TODO(b/288074305): Tracking bug.
-    @JvmField
-    val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+    @JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
 
     /** Migrate the status view from the notification panel to keyguard root view. */
     // TODO(b/291767565): Tracking bug.
-    @JvmField
-    val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
+    @JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
 
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
@@ -316,8 +287,7 @@
 
     // TODO(b/270223352): Tracking Bug
     @JvmField
-    val HIDE_SMARTSPACE_ON_DREAM_OVERLAY =
-        releasedFlag(404, "hide_smartspace_on_dream_overlay")
+    val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay")
 
     // TODO(b/271460958): Tracking Bug
     @JvmField
@@ -357,8 +327,7 @@
 
     /** Enables Font Scaling Quick Settings tile */
     // TODO(b/269341316): Tracking Bug
-    @JvmField
-    val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
+    @JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
 
     /** Enables new QS Edit Mode visual refresh */
     // TODO(b/269787742): Tracking Bug
@@ -367,13 +336,11 @@
 
     // 600- status bar
 
-
     // TODO(b/265892345): Tracking Bug
     val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
 
     // TODO(b/280426085): Tracking Bug
-    @JvmField
-    val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
+    @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
 
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
@@ -441,8 +408,8 @@
 
     // TODO(b/273509374): Tracking Bug
     @JvmField
-    val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
-        "always_show_home_controls_on_dreams")
+    val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
+        releasedFlag(1006, "always_show_home_controls_on_dreams")
 
     // 1100 - windowing
     @Keep
@@ -490,11 +457,7 @@
     @Keep
     @JvmField
     val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-        sysPropBooleanFlag(
-            1110,
-            "persist.wm.debug.enable_pip_keep_clear_algorithm",
-            default = true
-        )
+        sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
 
     // TODO(b/256873975): Tracking Bug
     @JvmField
@@ -532,7 +495,8 @@
 
     // TODO(b/273443374): Tracking Bug
     @Keep
-    @JvmField val LOCKSCREEN_LIVE_WALLPAPER =
+    @JvmField
+    val LOCKSCREEN_LIVE_WALLPAPER =
         sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
 
     // TODO(b/281648899): Tracking bug
@@ -547,7 +511,6 @@
     val ENABLE_PIP2_IMPLEMENTATION =
         sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
 
-
     // 1200 - predictive back
     @Keep
     @JvmField
@@ -573,8 +536,7 @@
         unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
 
     // TODO(b/270987164): Tracking Bug
-    @JvmField
-    val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
+    @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
 
     // TODO(b/263826204): Tracking Bug
     @JvmField
@@ -597,8 +559,7 @@
         unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
 
     // TODO(b/273800936): Tracking Bug
-    @JvmField
-    val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
+    @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
 
     // 1300 - screenshots
     // TODO(b/264916608): Tracking Bug
@@ -623,16 +584,12 @@
     // 1700 - clipboard
     @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
     // TODO(b/278714186) Tracking Bug
-    @JvmField val CLIPBOARD_IMAGE_TIMEOUT =
-            unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
+    @JvmField
+    val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
     // TODO(b/279405451): Tracking Bug
     @JvmField
     val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions")
 
-    // 1800 - shade container
-    // TODO(b/265944639): Tracking Bug
-    @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
-
     // TODO(b/283300105): Tracking Bug
     @JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
 
@@ -642,19 +599,15 @@
     // 2000 - device controls
     @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
 
-    @JvmField
-    val APP_PANELS_ALL_APPS_ALLOWED =
-        releasedFlag(2001, "app_panels_all_apps_allowed")
+    @JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed")
 
     @JvmField
-    val CONTROLS_MANAGEMENT_NEW_FLOWS =
-        releasedFlag(2002, "controls_management_new_flows")
+    val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows")
 
     // Enables removing app from Home control panel as a part of a new flow
     // TODO(b/269132640): Tracking Bug
     @JvmField
-    val APP_PANELS_REMOVE_APPS_ALLOWED =
-        releasedFlag(2003, "app_panels_remove_apps_allowed")
+    val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed")
 
     // 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
     // TODO(b/259264861): Tracking Bug
@@ -665,11 +618,9 @@
 
     // 2300 - stylus
     @JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
+    @JvmField val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
     @JvmField
-    val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
-    @JvmField
-    val ENABLE_USI_BATTERY_NOTIFICATIONS =
-        releasedFlag(2302, "enable_usi_battery_notifications")
+    val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications")
     @JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
 
     // 2400 - performance tools and debugging info
@@ -681,11 +632,10 @@
     // TODO(b/283071711): Tracking bug
     @JvmField
     val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
-            unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
+        unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
 
     // TODO:(b/283203305): Tracking bug
-    @JvmField
-    val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
+    @JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
 
     // 2700 - unfold transitions
     // TODO(b/265764985): Tracking Bug
@@ -708,27 +658,21 @@
     @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
 
     // TODO(b/259428678): Tracking Bug
-    @JvmField
-    val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
+    @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
 
     // TODO(b/277192623): Tracking Bug
-    @JvmField
-    val KEYBOARD_EDUCATION =
-        unreleasedFlag(2603, "keyboard_education", teamfood = false)
+    @JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false)
 
     // TODO(b/277201412): Tracking Bug
     @JvmField
-    val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
-            releasedFlag(2805, "split_shade_subpixel_optimization")
+    val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization")
 
     // TODO(b/288868056): Tracking Bug
     @JvmField
-    val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
-            unreleasedFlag(288868056, "pss_task_switcher")
+    val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher")
 
     // TODO(b/278761837): Tracking Bug
-    @JvmField
-    val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
+    @JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
 
     // 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name>
 
@@ -744,23 +688,20 @@
         unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream")
 
     // TODO(b/283084712): Tracking Bug
-    @JvmField
-    val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
+    @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
 
     // TODO(b/283447257): Tracking bug
     @JvmField
     val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
-            unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
+        unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
 
     // TODO(b/283740863): Tracking Bug
     @JvmField
     val ENABLE_NEW_PRIVACY_DIALOG =
-            unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
+        unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
 
     // TODO(b/289573946): Tracking Bug
-    @JvmField
-    val PRECOMPUTED_TEXT =
-        unreleasedFlag(289573946, "precomputed_text")
+    @JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
 
     // 2900 - CentralSurfaces-related flags
 
@@ -773,6 +714,5 @@
 
     // TODO(b/290213663): Tracking Bug
     @JvmField
-    val ONE_WAY_HAPTICS_API_MIGRATION =
-        unreleasedFlag(3100, "oneway_haptics_api_migration")
+    val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index e0834bb..82d0c8b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -444,6 +444,7 @@
     private final LockPatternUtils mLockPatternUtils;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private boolean mKeyguardDonePending = false;
+    private boolean mUnlockingAndWakingFromDream = false;
     private boolean mHideAnimationRun = false;
     private boolean mHideAnimationRunning = false;
 
@@ -802,6 +803,25 @@
             mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
             mKeyguardDisplayManager.hide();
             mUpdateMonitor.startBiometricWatchdog();
+
+            // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
+            // dreaming. It's time to wake up.
+            if (mUnlockingAndWakingFromDream) {
+                Log.d(TAG, "waking from dream after unlock");
+                mUnlockingAndWakingFromDream = false;
+
+                if (mKeyguardStateController.isShowing()) {
+                    Log.d(TAG, "keyguard showing after keyguardGone, dismiss");
+                    mKeyguardViewControllerLazy.get()
+                            .notifyKeyguardAuthenticated(!mWakeAndUnlocking);
+                } else {
+                    Log.d(TAG, "keyguard gone, waking up from dream");
+                    mPM.wakeUp(mSystemClock.uptimeMillis(),
+                            mWakeAndUnlocking ? PowerManager.WAKE_REASON_BIOMETRIC
+                            : PowerManager.WAKE_REASON_GESTURE,
+                            "com.android.systemui:UNLOCK_DREAMING");
+                }
+            }
             Trace.endSection();
         }
 
@@ -2673,6 +2693,7 @@
 
             mKeyguardExitAnimationRunner = null;
             mWakeAndUnlocking = false;
+            mUnlockingAndWakingFromDream = false;
             setPendingLock(false);
 
             // Force if we we're showing in the middle of hiding, to ensure we end up in the correct
@@ -2795,7 +2816,13 @@
         synchronized (KeyguardViewMediator.this) {
             if (DEBUG) Log.d(TAG, "handleHide");
 
-            if (mShowing && !mOccluded) {
+            mUnlockingAndWakingFromDream = mStatusBarStateController.isDreaming()
+                && !mStatusBarStateController.isDozing();
+
+            if ((mShowing && !mOccluded) || mUnlockingAndWakingFromDream) {
+                if (mUnlockingAndWakingFromDream) {
+                    Log.d(TAG, "hiding keyguard before waking from dream");
+                }
                 mHiding = true;
                 mKeyguardGoingAwayRunnable.run();
             } else {
@@ -2806,13 +2833,6 @@
                         mHideAnimation.getDuration());
                 onKeyguardExitFinished();
             }
-
-            // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
-            // dreaming. It's time to wake up.
-            if (mDreamOverlayShowing || mUpdateMonitor.isDreaming()) {
-                mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
-                        "com.android.systemui:UNLOCK_DREAMING");
-            }
         }
         Trace.endSection();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index d1f011e..bf1e75b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -102,6 +102,9 @@
     /** Whether bypass is currently enabled */
     val isBypassEnabled: Flow<Boolean>
 
+    /** Set whether face authentication should be locked out or not */
+    fun lockoutFaceAuth()
+
     /**
      * Trigger face authentication.
      *
@@ -199,6 +202,10 @@
         }
             ?: flowOf(false)
 
+    override fun lockoutFaceAuth() {
+        _isLockedOut.value = true
+    }
+
     private val faceLockoutResetCallback =
         object : FaceManager.LockoutResetCallback() {
             override fun onLockoutReset(sensorId: Int) {
@@ -396,7 +403,7 @@
     private val faceAuthCallback =
         object : FaceManager.AuthenticationCallback() {
             override fun onAuthenticationFailed() {
-                _authenticationStatus.value = FailedFaceAuthenticationStatus
+                _authenticationStatus.value = FailedFaceAuthenticationStatus()
                 _isAuthenticated.value = false
                 faceAuthLogger.authenticationFailed()
                 onFaceAuthRequestCompleted()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 482e9a3d..6a2511f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -20,10 +20,13 @@
 
 import android.content.Context
 import android.graphics.Point
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LiftReveal
 import com.android.systemui.statusbar.LightRevealEffect
@@ -31,9 +34,12 @@
 import javax.inject.Inject
 import kotlin.math.max
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -52,6 +58,10 @@
      * at the current screen position of the appropriate sensor.
      */
     val revealEffect: Flow<LightRevealEffect>
+
+    val revealAmount: Flow<Float>
+
+    fun startRevealAmountAnimator(reveal: Boolean)
 }
 
 @SysUISingleton
@@ -108,13 +118,30 @@
 
     /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */
     private val nonBiometricRevealEffect: Flow<LightRevealEffect?> =
-        keyguardRepository.wakefulness.flatMapLatest { wakefulnessModel ->
-            when {
-                wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
-                wakefulnessModel.isAwakeFromTap() -> tapRevealEffect
-                else -> flowOf(LiftReveal)
+        keyguardRepository.wakefulness
+            .filter { it.isStartingToWake() || it.isStartingToSleep() }
+            .flatMapLatest { wakefulnessModel ->
+                when {
+                    wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
+                    wakefulnessModel.isWakingFrom(TAP) -> tapRevealEffect
+                    else -> flowOf(LiftReveal)
+                }
             }
-        }
+
+    private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 }
+
+    override val revealAmount: Flow<Float> = callbackFlow {
+        val updateListener =
+            Animator.AnimatorUpdateListener {
+                trySend((it as ValueAnimator).animatedValue as Float)
+            }
+        revealAmountAnimator.addUpdateListener(updateListener)
+        awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) }
+    }
+
+    override fun startRevealAmountAnimator(reveal: Boolean) {
+        if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+    }
 
     override val revealEffect =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
index 27e3a74..5ef9a9e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
@@ -55,6 +55,8 @@
     override val isBypassEnabled: Flow<Boolean>
         get() = emptyFlow()
 
+    override fun lockoutFaceAuth() = Unit
+
     /**
      * Trigger face authentication.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
index 141b130..e57c919 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
@@ -60,6 +60,7 @@
     fun onNotificationPanelClicked()
     fun onSwipeUpOnBouncer()
     fun onPrimaryBouncerUserInput()
+    fun onAccessibilityAction()
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 833eda7..4244e55 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -17,28 +17,44 @@
 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.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 
 @ExperimentalCoroutinesApi
 @SysUISingleton
 class LightRevealScrimInteractor
 @Inject
 constructor(
-    transitionRepository: KeyguardTransitionRepository,
-    transitionInteractor: KeyguardTransitionInteractor,
-    lightRevealScrimRepository: LightRevealScrimRepository,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    private val lightRevealScrimRepository: LightRevealScrimRepository,
+    @Application private val scope: CoroutineScope,
 ) {
 
+    init {
+        listenForStartedKeyguardTransitionStep()
+    }
+
+    private fun listenForStartedKeyguardTransitionStep() {
+        scope.launch {
+            transitionInteractor.startedKeyguardTransitionStep.collect {
+                if (willTransitionChangeEndState(it)) {
+                    lightRevealScrimRepository.startRevealAmountAnimator(
+                        willBeRevealedInState(it.to)
+                    )
+                }
+            }
+        }
+    }
+
     /**
      * Whenever a keyguard transition starts, sample the latest reveal effect from the repository
      * and use that for the starting transition.
@@ -54,17 +70,7 @@
             lightRevealScrimRepository.revealEffect
         )
 
-    /**
-     * The reveal amount to use for the light reveal scrim, which is derived from the keyguard
-     * transition steps.
-     */
-    val revealAmount: Flow<Float> =
-        transitionRepository.transitions
-            // Only listen to transitions that change the reveal amount.
-            .filter { willTransitionAffectRevealAmount(it) }
-            // Use the transition amount as the reveal amount, inverting it if we're transitioning
-            // to a non-revealed (hidden) state.
-            .map { step -> if (willBeRevealedInState(step.to)) step.value else 1f - step.value }
+    val revealAmount = lightRevealScrimRepository.revealAmount
 
     companion object {
 
@@ -72,7 +78,7 @@
          * Whether the transition requires a change in the reveal amount of the light reveal scrim.
          * If not, we don't care about the transition and don't need to listen to it.
          */
-        fun willTransitionAffectRevealAmount(transition: TransitionStep): Boolean {
+        fun willTransitionChangeEndState(transition: TransitionStep): Boolean {
             return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
index 10dd900..596a1c0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
@@ -60,4 +60,5 @@
 
     override fun onSwipeUpOnBouncer() {}
     override fun onPrimaryBouncerUserInput() {}
+    override fun onAccessibilityAction() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index 6e7a20b..8f4776f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -67,6 +68,7 @@
     private val featureFlags: FeatureFlags,
     private val faceAuthenticationLogger: FaceAuthenticationLogger,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
 ) : CoreStartable, KeyguardFaceAuthInteractor {
 
     private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
@@ -117,6 +119,15 @@
                 )
             }
             .launchIn(applicationScope)
+
+        deviceEntryFingerprintAuthRepository.isLockedOut
+            .onEach {
+                if (it) {
+                    faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
+                    repository.lockoutFaceAuth()
+                }
+            }
+            .launchIn(applicationScope)
     }
 
     override fun onSwipeUpOnBouncer() {
@@ -143,6 +154,10 @@
         runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
     }
 
+    override fun onAccessibilityAction() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false)
+    }
+
     override fun registerListener(listener: FaceAuthenticationListener) {
         listeners.add(listener)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index d9792cf..3de3666 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -23,29 +23,40 @@
  * Authentication status provided by
  * [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository]
  */
-sealed class FaceAuthenticationStatus(
-    // present to break equality check if the same error occurs repeatedly.
-    val createdAt: Long = elapsedRealtime()
-)
+sealed class FaceAuthenticationStatus
 
 /** Success authentication status. */
-data class SuccessFaceAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
-    FaceAuthenticationStatus()
+data class SuccessFaceAuthenticationStatus(
+    val successResult: FaceManager.AuthenticationResult,
+    // present to break equality check if the same error occurs repeatedly.
+    @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
 
 /** Face authentication help message. */
-data class HelpFaceAuthenticationStatus(val msgId: Int, val msg: String?) :
-    FaceAuthenticationStatus()
+data class HelpFaceAuthenticationStatus(
+    val msgId: Int,
+    val msg: String?, // present to break equality check if the same error occurs repeatedly.
+    @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
 
 /** Face acquired message. */
-data class AcquiredFaceAuthenticationStatus(val acquiredInfo: Int) : FaceAuthenticationStatus()
+data class AcquiredFaceAuthenticationStatus(
+    val acquiredInfo: Int, // present to break equality check if the same error occurs repeatedly.
+    @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
 
 /** Face authentication failed message. */
-object FailedFaceAuthenticationStatus : FaceAuthenticationStatus()
+data class FailedFaceAuthenticationStatus(
+    // present to break equality check if the same error occurs repeatedly.
+    @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
 
 /** Face authentication error message */
 data class ErrorFaceAuthenticationStatus(
     val msgId: Int,
     val msg: String? = null,
+    // present to break equality check if the same error occurs repeatedly.
+    @JvmField val createdAt: Long = elapsedRealtime()
 ) : FaceAuthenticationStatus() {
     /**
      * Method that checks if [msgId] is a lockout error. A lockout error means that face
@@ -80,5 +91,5 @@
     val userId: Int,
     val isStrongBiometric: Boolean,
     // present to break equality check if the same error occurs repeatedly.
-    val createdAt: Long = elapsedRealtime()
+    @JvmField val createdAt: Long = elapsedRealtime()
 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
index cfd9e08..62f43ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -16,6 +16,13 @@
 package com.android.systemui.keyguard.shared.model
 
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.GESTURE
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.POWER_BUTTON
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.ASLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.AWAKE
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_SLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_WAKE
 
 /** Model device wakefulness states. */
 data class WakefulnessModel(
@@ -23,33 +30,31 @@
     val lastWakeReason: WakeSleepReason,
     val lastSleepReason: WakeSleepReason,
 ) {
-    fun isStartingToWake() = state == WakefulnessState.STARTING_TO_WAKE
+    fun isStartingToWake() = state == STARTING_TO_WAKE
 
-    fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP
+    fun isStartingToSleep() = state == STARTING_TO_SLEEP
 
-    private fun isAsleep() = state == WakefulnessState.ASLEEP
+    private fun isAsleep() = state == ASLEEP
+
+    private fun isAwake() = state == AWAKE
+
+    fun isStartingToWakeOrAwake() = isStartingToWake() || isAwake()
 
     fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep()
 
     fun isDeviceInteractive() = !isAsleep()
 
-    fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE
+    fun isWakingFrom(wakeSleepReason: WakeSleepReason) =
+        isStartingToWake() && lastWakeReason == wakeSleepReason
 
-    fun isStartingToSleepFromPowerButton() =
-        isStartingToSleep() && lastWakeReason == WakeSleepReason.POWER_BUTTON
-
-    fun isWakingFromPowerButton() =
-        isStartingToWake() && lastWakeReason == WakeSleepReason.POWER_BUTTON
+    fun isStartingToSleepFrom(wakeSleepReason: WakeSleepReason) =
+        isStartingToSleep() && lastSleepReason == wakeSleepReason
 
     fun isTransitioningFromPowerButton() =
-        isStartingToSleepFromPowerButton() || isWakingFromPowerButton()
-
-    fun isAwakeFromTap() =
-        state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP
+        isStartingToSleepFrom(POWER_BUTTON) || isWakingFrom(POWER_BUTTON)
 
     fun isDeviceInteractiveFromTapOrGesture(): Boolean {
-        return isDeviceInteractive() &&
-            (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
+        return isDeviceInteractive() && (lastWakeReason == TAP || lastWakeReason == GESTURE)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index 389cf76..f1ceaaa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -20,20 +20,18 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 /** View-model for the keyguard indication area view */
-@OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardIndicationAreaViewModel
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
-    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
-    private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
+    bottomAreaInteractor: KeyguardBottomAreaInteractor,
+    keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val burnInHelperWrapper: BurnInHelperWrapper,
 ) {
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
index a46d441..82f40bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
@@ -19,12 +19,14 @@
 import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor
 import com.android.systemui.statusbar.LightRevealEffect
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 
 /**
  * Models UI state for the light reveal scrim, which is used during screen on and off animations to
  * draw a gradient that reveals/hides the contents of the screen.
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 class LightRevealScrimViewModel @Inject constructor(interactor: LightRevealScrimInteractor) {
     val lightRevealEffect: Flow<LightRevealEffect> = interactor.lightRevealEffect
     val revealAmount: Flow<Float> = interactor.revealAmount
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 373f705..66067b1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -8,6 +8,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.FaceAuthLog
+import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
 private const val TAG = "DeviceEntryFaceAuthRepositoryLog"
@@ -264,4 +265,8 @@
     fun watchdogScheduled() {
         logBuffer.log(TAG, DEBUG, "FaceManager Biometric watchdog scheduled.")
     }
+
+    fun faceLockedOut(@CompileTimeConstant reason: String) {
+        logBuffer.log(TAG, DEBUG, "Face auth has been locked out: $reason")
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index f6a2f37..72352e3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -330,6 +330,8 @@
         // Don't send cancel if the user has moved on to the next activity.
         if (!mUserSelectingTask) {
             finish(RECORD_CANCEL, /* projection= */ null);
+        } else {
+            super.finish();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
new file mode 100644
index 0000000..c74a71c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.model
+
+import com.android.systemui.dagger.qualifiers.DisplayId
+
+/**
+ * In-bulk updates multiple flag values and commits the update.
+ *
+ * Example:
+ * ```
+ * sysuiState.updateFlags(
+ *     displayId,
+ *     SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ *     SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ *     SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ *     SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ *     SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == SceneKey.Lockscreen),
+ * )
+ * ```
+ *
+ * You can inject [displayId] by injecting it using:
+ * ```
+ *     @DisplayId private val displayId: Int`,
+ * ```
+ */
+fun SysUiState.updateFlags(
+    @DisplayId displayId: Int,
+    vararg flagValuePairs: Pair<Int, Boolean>,
+) {
+    flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
+    commitUpdate(displayId)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
deleted file mode 100644
index c48028c..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.model
-
-import com.android.systemui.multishade.shared.model.ShadeId
-
-/** Models the current interaction with one of the shades. */
-data class MultiShadeInteractionModel(
-    /** The ID of the shade that the user is currently interacting with. */
-    val shadeId: ShadeId,
-    /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
-    val isProxied: Boolean,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
deleted file mode 100644
index 86f0c0d..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.remoteproxy
-
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-
-/**
- * Acts as a hub for routing proxied user input into the multi shade system.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself. This class
- * is that proxy.
- */
-@Singleton
-class MultiShadeInputProxy @Inject constructor() {
-    private val _proxiedTouch =
-        MutableSharedFlow<ProxiedInputModel>(
-            replay = 1,
-            onBufferOverflow = BufferOverflow.DROP_OLDEST,
-        )
-    val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()
-
-    fun onProxiedInput(proxiedInput: ProxiedInputModel) {
-        _proxiedTouch.tryEmit(proxiedInput)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
deleted file mode 100644
index 1172030..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.repository
-
-import android.content.Context
-import androidx.annotation.FloatRange
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Encapsulates application state for all shades. */
-@SysUISingleton
-class MultiShadeRepository
-@Inject
-constructor(
-    @Application private val applicationContext: Context,
-    inputProxy: MultiShadeInputProxy,
-) {
-    /**
-     * Remote input coming from sources outside of system UI (for example, swiping down on the
-     * Launcher or from the status bar).
-     */
-    val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput
-
-    /** Width of the left-hand side shade, in pixels. */
-    private val leftShadeWidthPx =
-        applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
-
-    /** Width of the right-hand side shade, in pixels. */
-    private val rightShadeWidthPx =
-        applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
-
-    /**
-     * The amount that the user must swipe up when the shade is fully expanded to automatically
-     * collapse once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully expanded once the user stops swiping.
-     *
-     * This is a fraction between `0` and `1`.
-     */
-    private val swipeCollapseThreshold =
-        checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
-
-    /**
-     * The amount that the user must swipe down when the shade is fully collapsed to automatically
-     * expand once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully collapsed once the user stops swiping.
-     *
-     * This is a fraction between `0` and `1`.
-     */
-    private val swipeExpandThreshold =
-        checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
-
-    /**
-     * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
-     *
-     * This is a fraction between `0` and `1`.
-     */
-    private val dualShadeScrimAlpha =
-        checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
-
-    /** The current configuration of the shade system. */
-    val shadeConfig: StateFlow<ShadeConfig> =
-        MutableStateFlow(
-                if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
-                    ShadeConfig.DualShadeConfig(
-                        leftShadeWidthPx = leftShadeWidthPx,
-                        rightShadeWidthPx = rightShadeWidthPx,
-                        swipeCollapseThreshold = swipeCollapseThreshold,
-                        swipeExpandThreshold = swipeExpandThreshold,
-                        splitFraction =
-                            applicationContext.resources.getFloat(
-                                R.dimen.dual_shade_split_fraction
-                            ),
-                        scrimAlpha = dualShadeScrimAlpha,
-                    )
-                } else {
-                    ShadeConfig.SingleShadeConfig(
-                        swipeCollapseThreshold = swipeCollapseThreshold,
-                        swipeExpandThreshold = swipeExpandThreshold,
-                    )
-                }
-            )
-            .asStateFlow()
-
-    private val _forceCollapseAll = MutableStateFlow(false)
-    /** Whether all shades should be collapsed. */
-    val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()
-
-    private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
-    /** The current shade interaction or `null` if no shade is interacted with currently. */
-    val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()
-
-    private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()
-
-    /** The model for the shade with the given ID. */
-    fun getShade(
-        shadeId: ShadeId,
-    ): StateFlow<ShadeModel> {
-        return getMutableShade(shadeId).asStateFlow()
-    }
-
-    /** Sets the expansion amount for the shade with the given ID. */
-    fun setExpansion(
-        shadeId: ShadeId,
-        @FloatRange(from = 0.0, to = 1.0) expansion: Float,
-    ) {
-        getMutableShade(shadeId).let { mutableState ->
-            mutableState.value = mutableState.value.copy(expansion = expansion)
-        }
-    }
-
-    /** Sets whether all shades should be immediately forced to collapse. */
-    fun setForceCollapseAll(isForced: Boolean) {
-        _forceCollapseAll.value = isForced
-    }
-
-    /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
-    fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
-        _shadeInteraction.value = shadeInteraction
-    }
-
-    private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
-        return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
-    }
-
-    /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
-    private fun checkInBounds(float: Float): Float {
-        check(float in 0f..1f) { "$float isn't between 0 and 1." }
-        return float
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
deleted file mode 100644
index ebb8639..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import androidx.annotation.FloatRange
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.yield
-
-/** Encapsulates business logic related to interactions with the multi-shade system. */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MultiShadeInteractor
-@Inject
-constructor(
-    @Application private val applicationScope: CoroutineScope,
-    private val repository: MultiShadeRepository,
-    private val inputProxy: MultiShadeInputProxy,
-) {
-    /** The current configuration of the shade system. */
-    val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
-
-    /** The expansion of the shade that's most expanded. */
-    val maxShadeExpansion: Flow<Float> =
-        repository.shadeConfig.flatMapLatest { shadeConfig ->
-            combine(allShades(shadeConfig)) { shadeModels ->
-                shadeModels.maxOfOrNull { it.expansion } ?: 0f
-            }
-        }
-
-    /** Whether any shade is expanded, even a little bit. */
-    val isAnyShadeExpanded: Flow<Boolean> =
-        maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged()
-
-    /**
-     * A _processed_ version of the proxied input flow.
-     *
-     * All internal dependencies on the proxied input flow *must* use this one for two reasons:
-     * 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
-     *    actually have.
-     * 2. It actually does some preprocessing as the proxied input events stream through, handling
-     *    common things like recording the current state of the system based on incoming input
-     *    events.
-     */
-    private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
-        combine(
-                repository.shadeConfig,
-                repository.proxiedInput.distinctUntilChanged(),
-                ::Pair,
-            )
-            .map { (shadeConfig, proxiedInput) ->
-                if (proxiedInput !is ProxiedInputModel.OnTap) {
-                    // If the user is interacting with any other gesture type (for instance,
-                    // dragging),
-                    // we no longer want to force collapse all shades.
-                    repository.setForceCollapseAll(false)
-                }
-
-                when (proxiedInput) {
-                    is ProxiedInputModel.OnDrag -> {
-                        val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
-                        // This might be the start of a new drag gesture, let's update our
-                        // application
-                        // state to record that fact.
-                        onUserInteractionStarted(
-                            shadeId = affectedShadeId,
-                            isProxied = true,
-                        )
-                    }
-                    is ProxiedInputModel.OnTap -> {
-                        // Tapping outside any shade collapses all shades. This code path is not hit
-                        // for
-                        // taps that happen _inside_ a shade as that input event is directly applied
-                        // through the UI and is, hence, not a proxied input.
-                        collapseAll()
-                    }
-                    else -> Unit
-                }
-
-                proxiedInput
-            }
-            .shareIn(
-                scope = applicationScope,
-                started = SharingStarted.Eagerly,
-                replay = 1,
-            )
-
-    /** Whether the shade with the given ID should be visible. */
-    fun isVisible(shadeId: ShadeId): Flow<Boolean> {
-        return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
-    }
-
-    /** Whether direct user input is allowed on the shade with the given ID. */
-    fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
-        return combine(
-                isForceCollapsed(shadeId),
-                repository.shadeInteraction,
-                ::Pair,
-            )
-            .map { (isForceCollapsed, shadeInteraction) ->
-                !isForceCollapsed && shadeInteraction?.isProxied != true
-            }
-    }
-
-    /** Whether the shade with the given ID is forced to collapse. */
-    fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
-        return combine(
-                repository.forceCollapseAll,
-                repository.shadeInteraction.map { it?.shadeId },
-                ::Pair,
-            )
-            .map { (collapseAll, userInteractedShadeIdOrNull) ->
-                val counterpartShadeIdOrNull =
-                    when (shadeId) {
-                        ShadeId.SINGLE -> null
-                        ShadeId.LEFT -> ShadeId.RIGHT
-                        ShadeId.RIGHT -> ShadeId.LEFT
-                    }
-
-                when {
-                    // If all shades have been told to collapse (by a tap outside, for example),
-                    // then this shade is collapsed.
-                    collapseAll -> true
-                    // A shade that doesn't have a counterpart shade cannot be force-collapsed by
-                    // interactions on the counterpart shade.
-                    counterpartShadeIdOrNull == null -> false
-                    // If the current user interaction is on the counterpart shade, then this shade
-                    // should be force-collapsed.
-                    else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
-                }
-            }
-    }
-
-    /**
-     * Proxied input affecting the shade with the given ID. This is input coming from sources
-     * outside of system UI (for example, swiping down on the Launcher or from the status bar) or
-     * outside the UI of any shade (for example, the scrim that's shown behind the shades).
-     */
-    fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
-        return combine(
-                processedProxiedInput,
-                isForceCollapsed(shadeId).distinctUntilChanged(),
-                repository.shadeInteraction,
-                ::Triple,
-            )
-            .map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
-                when {
-                    // If the shade is force-collapsed, we ignored proxied input on it.
-                    isForceCollapsed -> null
-                    // If the proxied input does not belong to this shade, ignore it.
-                    shadeInteraction?.shadeId != shadeId -> null
-                    // If there is ongoing non-proxied user input on any shade, ignore the
-                    // proxied input.
-                    !shadeInteraction.isProxied -> null
-                    // Otherwise, send the proxied input downstream.
-                    else -> proxiedInput
-                }
-            }
-            .onEach { proxiedInput ->
-                // We use yield() to make sure that the following block of code happens _after_
-                // downstream collectors had a chance to process the proxied input. Otherwise, we
-                // might change our state to clear the current UserInteraction _before_ those
-                // downstream collectors get a chance to process the proxied input, which will make
-                // them ignore it (since they ignore proxied input when the current user interaction
-                // doesn't match their shade).
-                yield()
-
-                if (
-                    proxiedInput is ProxiedInputModel.OnDragEnd ||
-                        proxiedInput is ProxiedInputModel.OnDragCancel
-                ) {
-                    onUserInteractionEnded(shadeId = shadeId, isProxied = true)
-                }
-            }
-    }
-
-    /** Sets the expansion amount for the shade with the given ID. */
-    fun setExpansion(
-        shadeId: ShadeId,
-        @FloatRange(from = 0.0, to = 1.0) expansion: Float,
-    ) {
-        repository.setExpansion(shadeId, expansion)
-    }
-
-    /** Collapses all shades. */
-    fun collapseAll() {
-        repository.setForceCollapseAll(true)
-    }
-
-    /**
-     * Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
-     * the same interaction as it won't overwrite an existing interaction.
-     *
-     * Existing interactions can be cleared by calling [onUserInteractionEnded].
-     */
-    fun onUserInteractionStarted(shadeId: ShadeId) {
-        onUserInteractionStarted(
-            shadeId = shadeId,
-            isProxied = false,
-        )
-    }
-
-    /**
-     * Notifies that the current non-proxied interaction has ended.
-     *
-     * Safe to call multiple times, even if there's no current interaction or even if the current
-     * interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
-     * there's a match between the parameters and the current interaction.
-     */
-    fun onUserInteractionEnded(
-        shadeId: ShadeId,
-    ) {
-        onUserInteractionEnded(
-            shadeId = shadeId,
-            isProxied = false,
-        )
-    }
-
-    fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
-        inputProxy.onProxiedInput(proxiedInput)
-    }
-
-    /**
-     * Notifies that a new interaction may have started. Safe to call multiple times for the same
-     * interaction as it won't overwrite an existing interaction.
-     *
-     * Existing interactions can be cleared by calling [onUserInteractionEnded].
-     */
-    private fun onUserInteractionStarted(
-        shadeId: ShadeId,
-        isProxied: Boolean,
-    ) {
-        if (repository.shadeInteraction.value != null) {
-            return
-        }
-
-        repository.setShadeInteraction(
-            MultiShadeInteractionModel(
-                shadeId = shadeId,
-                isProxied = isProxied,
-            )
-        )
-    }
-
-    /**
-     * Notifies that the current interaction has ended.
-     *
-     * Safe to call multiple times, even if there's no current interaction or even if the current
-     * interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
-     * unless there's a match between the parameters and the current interaction.
-     */
-    private fun onUserInteractionEnded(
-        shadeId: ShadeId,
-        isProxied: Boolean,
-    ) {
-        repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
-            if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
-                repository.setShadeInteraction(null)
-            }
-        }
-    }
-
-    /**
-     * Returns the ID of the shade that's affected by user input at a given coordinate.
-     *
-     * @param config The shade configuration being used.
-     * @param xFraction The horizontal position of the user input as a fraction along the width of
-     *   its container where `0` is all the way to the left and `1` is all the way to the right.
-     */
-    private fun affectedShadeId(
-        config: ShadeConfig,
-        @FloatRange(from = 0.0, to = 1.0) xFraction: Float,
-    ): ShadeId {
-        return if (config is ShadeConfig.DualShadeConfig) {
-            if (xFraction <= config.splitFraction) {
-                ShadeId.LEFT
-            } else {
-                ShadeId.RIGHT
-            }
-        } else {
-            ShadeId.SINGLE
-        }
-    }
-
-    /** Returns the list of flows of all the shades in the given configuration. */
-    private fun allShades(
-        config: ShadeConfig,
-    ): List<Flow<ShadeModel>> {
-        return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
deleted file mode 100644
index 1894bc4..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.content.Context
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import com.android.systemui.classifier.Classifier
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.shade.ShadeController
-import javax.inject.Inject
-import kotlin.math.abs
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-
-/**
- * Encapsulates business logic to handle [MotionEvent]-based user input.
- *
- * This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s
- * into the newer multi-shade framework for processing.
- */
-@SysUISingleton
-class MultiShadeMotionEventInteractor
-@Inject
-constructor(
-    @Application private val applicationContext: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val multiShadeInteractor: MultiShadeInteractor,
-    featureFlags: FeatureFlags,
-    keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    private val falsingManager: FalsingManager,
-    private val shadeController: ShadeController,
-) {
-    init {
-        if (featureFlags.isEnabled(Flags.DUAL_SHADE)) {
-            applicationScope.launch {
-                multiShadeInteractor.isAnyShadeExpanded.collect {
-                    if (!it && !shadeController.isKeyguard) {
-                        shadeController.makeExpandedInvisible()
-                    } else {
-                        shadeController.makeExpandedVisible(false)
-                    }
-                }
-            }
-        }
-    }
-
-    private val isAnyShadeExpanded: StateFlow<Boolean> =
-        multiShadeInteractor.isAnyShadeExpanded.stateIn(
-            scope = applicationScope,
-            started = SharingStarted.Eagerly,
-            initialValue = false,
-        )
-
-    private val isBouncerShowing: StateFlow<Boolean> =
-        keyguardTransitionInteractor
-            .transitionValue(state = KeyguardState.PRIMARY_BOUNCER)
-            .map { !it.isZero() }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.Eagerly,
-                initialValue = false,
-            )
-
-    private var interactionState: InteractionState? = null
-
-    /**
-     * Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be
-     * passed to this interactor's [onTouchEvent] method.
-     *
-     * Note: the caller should continue to pass [MotionEvent] instances into this method, even if it
-     * returns `false` as the gesture may be intercepted mid-stream.
-     */
-    fun shouldIntercept(event: MotionEvent): Boolean {
-        if (isAnyShadeExpanded.value) {
-            // If any shade is expanded, we assume that touch handling outside the shades is handled
-            // by the scrim that appears behind the shades. No need to intercept anything here.
-            return false
-        }
-
-        if (isBouncerShowing.value) {
-            return false
-        }
-
-        return when (event.actionMasked) {
-            MotionEvent.ACTION_DOWN -> {
-                // Record where the pointer was placed and which pointer it was.
-                interactionState =
-                    InteractionState(
-                        initialX = event.x,
-                        initialY = event.y,
-                        currentY = event.y,
-                        pointerId = event.getPointerId(0),
-                        isDraggingHorizontally = false,
-                        isDraggingShade = false,
-                    )
-
-                false
-            }
-            MotionEvent.ACTION_MOVE -> {
-                onMove(event)
-
-                // We want to intercept the rest of the gesture if we're dragging the shade.
-                isDraggingShade()
-            }
-            MotionEvent.ACTION_UP,
-            MotionEvent.ACTION_CANCEL ->
-                // Make sure that we intercept the up or cancel if we're dragging the shade, to
-                // handle drag end or cancel.
-                isDraggingShade()
-            else -> false
-        }
-    }
-
-    /**
-     * Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to
-     * the result of [shouldIntercept] has been received.
-     *
-     * @param event The [MotionEvent] to handle.
-     * @param viewWidthPx The width of the view, in pixels.
-     * @return `true` if the event was consumed, `false` otherwise.
-     */
-    fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean {
-        return when (event.actionMasked) {
-            MotionEvent.ACTION_MOVE -> {
-                interactionState?.let {
-                    if (it.isDraggingShade) {
-                        val pointerIndex = event.findPointerIndex(it.pointerId)
-                        val previousY = it.currentY
-                        val currentY = event.getY(pointerIndex)
-                        interactionState = it.copy(currentY = currentY)
-
-                        val yDragAmountPx = currentY - previousY
-
-                        if (yDragAmountPx != 0f) {
-                            multiShadeInteractor.sendProxiedInput(
-                                ProxiedInputModel.OnDrag(
-                                    xFraction = event.x / viewWidthPx,
-                                    yDragAmountPx = yDragAmountPx,
-                                )
-                            )
-                        }
-                        true
-                    } else {
-                        onMove(event)
-                        isDraggingShade()
-                    }
-                }
-                    ?: false
-            }
-            MotionEvent.ACTION_UP -> {
-                if (isDraggingShade()) {
-                    // We finished dragging the shade. Record that so the multi-shade framework can
-                    // issue a fling, if the velocity reached in the drag was high enough, for
-                    // example.
-                    multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
-
-                    if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
-                        multiShadeInteractor.collapseAll()
-                    }
-                }
-
-                interactionState = null
-                true
-            }
-            MotionEvent.ACTION_POINTER_UP -> {
-                val removedPointerId = event.getPointerId(event.actionIndex)
-                if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) {
-                    // We removed the original pointer but there must be another pointer because the
-                    // gesture is still ongoing. Let's switch to that pointer.
-                    interactionState =
-                        event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId
-                            ->
-                            interactionState?.copy(
-                                pointerId = replacementPointerId,
-                                // We want to update the currentY of our state so that the
-                                // transition to the next pointer doesn't report a big jump between
-                                // the Y coordinate of the removed pointer and the Y coordinate of
-                                // the replacement pointer.
-                                currentY = event.getY(replacementPointerId),
-                            )
-                        }
-                }
-                true
-            }
-            MotionEvent.ACTION_CANCEL -> {
-                if (isDraggingShade()) {
-                    // Our drag gesture was canceled by the system. This happens primarily in one of
-                    // two occasions: (a) the parent view has decided to intercept the gesture
-                    // itself and/or route it to a different child view or (b) the pointer has
-                    // traveled beyond the bounds of our view and/or the touch display. Either way,
-                    // we pass the cancellation event to the multi-shade framework to record it.
-                    // Doing that allows the multi-shade framework to know that the gesture ended to
-                    // allow new gestures to be accepted.
-                    multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
-
-                    if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
-                        multiShadeInteractor.collapseAll()
-                    }
-                }
-
-                interactionState = null
-                true
-            }
-            else -> false
-        }
-    }
-
-    /**
-     * Handles [MotionEvent.ACTION_MOVE] and sets whether or not we are dragging shade in our
-     * current interaction
-     *
-     * @param event The [MotionEvent] to handle.
-     */
-    private fun onMove(event: MotionEvent) {
-        interactionState?.let {
-            val pointerIndex = event.findPointerIndex(it.pointerId)
-            val currentX = event.getX(pointerIndex)
-            val currentY = event.getY(pointerIndex)
-            if (!it.isDraggingHorizontally && !it.isDraggingShade) {
-                val xDistanceTravelled = currentX - it.initialX
-                val yDistanceTravelled = currentY - it.initialY
-                val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop
-                interactionState =
-                    when {
-                        yDistanceTravelled > touchSlop -> it.copy(isDraggingShade = true)
-                        abs(xDistanceTravelled) > touchSlop ->
-                            it.copy(isDraggingHorizontally = true)
-                        else -> interactionState
-                    }
-            }
-        }
-    }
-
-    private data class InteractionState(
-        val initialX: Float,
-        val initialY: Float,
-        val currentY: Float,
-        val pointerId: Int,
-        /** Whether the current gesture is dragging horizontally. */
-        val isDraggingHorizontally: Boolean,
-        /** Whether the current gesture is dragging the shade vertically. */
-        val isDraggingShade: Boolean,
-    )
-
-    private fun isDraggingShade(): Boolean {
-        return interactionState?.isDraggingShade ?: false
-    }
-
-    /**
-     * Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is
-     * no other pointer.
-     */
-    private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? {
-        return (0 until pointerCount)
-            .firstOrNull { pointerIndex ->
-                val pointerId = getPointerId(pointerIndex)
-                pointerId != removedPointerId
-            }
-            ?.let { pointerIndex -> getPointerId(pointerIndex) }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt
deleted file mode 100644
index c2eaf72..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.shared.math
-
-import androidx.annotation.VisibleForTesting
-import kotlin.math.abs
-
-/** Returns `true` if this [Float] is within [epsilon] of `0`. */
-fun Float.isZero(epsilon: Float = EPSILON): Boolean {
-    return abs(this) < epsilon
-}
-
-@VisibleForTesting private const val EPSILON = 0.0001f
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
deleted file mode 100644
index ee1dd65..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/**
- * Models a part of an ongoing proxied user input gesture.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself.
- */
-sealed class ProxiedInputModel {
-    /** The user is dragging their pointer. */
-    data class OnDrag(
-        /**
-         * The relative position of the pointer as a fraction of its container width where `0` is
-         * all the way to the left and `1` is all the way to the right.
-         */
-        @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
-        /** The amount that the pointer was dragged, in pixels. */
-        val yDragAmountPx: Float,
-    ) : ProxiedInputModel()
-
-    /** The user finished dragging by lifting up their pointer. */
-    object OnDragEnd : ProxiedInputModel()
-
-    /**
-     * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
-     */
-    object OnDragCancel : ProxiedInputModel()
-
-    /** The user has tapped (clicked). */
-    object OnTap : ProxiedInputModel()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
deleted file mode 100644
index a4cd35c..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/** Enumerates the various possible configurations of the shade system. */
-sealed class ShadeConfig(
-
-    /** IDs of the shade(s) in this configuration. */
-    open val shadeIds: List<ShadeId>,
-
-    /**
-     * The amount that the user must swipe up when the shade is fully expanded to automatically
-     * collapse once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully expanded once the user stops swiping.
-     */
-    @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
-
-    /**
-     * The amount that the user must swipe down when the shade is fully collapsed to automatically
-     * expand once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully collapsed once the user stops swiping.
-     */
-    @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
-) {
-
-    /** There is a single shade. */
-    data class SingleShadeConfig(
-        @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
-        @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
-    ) :
-        ShadeConfig(
-            shadeIds = listOf(ShadeId.SINGLE),
-            swipeCollapseThreshold = swipeCollapseThreshold,
-            swipeExpandThreshold = swipeExpandThreshold,
-        )
-
-    /** There are two shades arranged side-by-side. */
-    data class DualShadeConfig(
-        /** Width of the left-hand side shade. */
-        val leftShadeWidthPx: Int,
-        /** Width of the right-hand side shade. */
-        val rightShadeWidthPx: Int,
-        @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
-        @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
-        /**
-         * The position of the "split" between interaction areas for each of the shades, as a
-         * fraction of the width of the container.
-         *
-         * Interactions that occur on the start-side (left-hand side in left-to-right languages like
-         * English) affect the start-side shade. Interactions that occur on the end-side (right-hand
-         * side in left-to-right languages like English) affect the end-side shade.
-         */
-        @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
-        /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
-        @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
-    ) :
-        ShadeConfig(
-            shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
-            swipeCollapseThreshold = swipeCollapseThreshold,
-            swipeExpandThreshold = swipeExpandThreshold,
-        )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
deleted file mode 100644
index 9e02657..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.shared.model
-
-/** Enumerates all known shade IDs. */
-enum class ShadeId {
-    /** ID of the shade on the left in dual shade configurations. */
-    LEFT,
-    /** ID of the shade on the right in dual shade configurations. */
-    RIGHT,
-    /** ID of the single shade in single shade configurations. */
-    SINGLE,
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
deleted file mode 100644
index aecec39..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.view
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.compose.ComposeFacade
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.launch
-
-/**
- * View that hosts the multi-shade system and acts as glue between legacy code and the
- * implementation.
- */
-class MultiShadeView(
-    context: Context,
-    attrs: AttributeSet?,
-) :
-    FrameLayout(
-        context,
-        attrs,
-    ) {
-
-    fun init(
-        interactor: MultiShadeInteractor,
-        clock: SystemClock,
-    ) {
-        repeatWhenAttached {
-            lifecycleScope.launch {
-                repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    addView(
-                        ComposeFacade.createMultiShadeView(
-                            context = context,
-                            viewModel =
-                                MultiShadeViewModel(
-                                    viewModelScope = this,
-                                    interactor = interactor,
-                                ),
-                            clock = clock,
-                        )
-                    )
-                }
-
-                // Here when destroyed.
-                removeAllViews()
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
deleted file mode 100644
index ed92c54..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for UI that supports multi (or single) shade. */
-@OptIn(ExperimentalCoroutinesApi::class)
-class MultiShadeViewModel(
-    viewModelScope: CoroutineScope,
-    private val interactor: MultiShadeInteractor,
-) {
-    /** Models UI state for the single shade. */
-    val singleShade =
-        ShadeViewModel(
-            viewModelScope,
-            ShadeId.SINGLE,
-            interactor,
-        )
-
-    /** Models UI state for the shade on the left-hand side. */
-    val leftShade =
-        ShadeViewModel(
-            viewModelScope,
-            ShadeId.LEFT,
-            interactor,
-        )
-
-    /** Models UI state for the shade on the right-hand side. */
-    val rightShade =
-        ShadeViewModel(
-            viewModelScope,
-            ShadeId.RIGHT,
-            interactor,
-        )
-
-    /** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
-    val scrimAlpha: StateFlow<Float> =
-        combine(
-                interactor.maxShadeExpansion,
-                interactor.shadeConfig
-                    .map { it as? ShadeConfig.DualShadeConfig }
-                    .map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
-                ::Pair,
-            )
-            .map { (anyShadeExpansion, scrimAlpha) ->
-                (anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
-            }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = 0f,
-            )
-
-    /** Whether the scrim should accept touch events. */
-    val isScrimEnabled: StateFlow<Boolean> =
-        interactor.shadeConfig
-            .flatMapLatest { shadeConfig ->
-                when (shadeConfig) {
-                    // In the dual shade configuration, the scrim is enabled when the expansion is
-                    // greater than zero on any one of the shades.
-                    is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded
-                    // No scrim in the single shade configuration.
-                    is ShadeConfig.SingleShadeConfig -> flowOf(false)
-                }
-            }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
-    /** Notifies that the scrim has been touched. */
-    fun onScrimTouched(proxiedInput: ProxiedInputModel) {
-        if (!isScrimEnabled.value) {
-            return
-        }
-
-        interactor.sendProxiedInput(proxiedInput)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
deleted file mode 100644
index e828dbd..0000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.annotation.FloatRange
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for a single shade. */
-class ShadeViewModel(
-    viewModelScope: CoroutineScope,
-    private val shadeId: ShadeId,
-    private val interactor: MultiShadeInteractor,
-) {
-    /** Whether the shade is visible. */
-    val isVisible: StateFlow<Boolean> =
-        interactor
-            .isVisible(shadeId)
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
-    /** Whether swiping on the shade UI is currently enabled. */
-    val isSwipingEnabled: StateFlow<Boolean> =
-        interactor
-            .isNonProxiedInputAllowed(shadeId)
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
-    /** Whether the shade must be collapsed immediately. */
-    val isForceCollapsed: Flow<Boolean> =
-        interactor.isForceCollapsed(shadeId).distinctUntilChanged()
-
-    /** The width of the shade. */
-    val width: StateFlow<Size> =
-        interactor.shadeConfig
-            .map { shadeWidth(it) }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = shadeWidth(interactor.shadeConfig.value),
-            )
-
-    /**
-     * The amount that the user must swipe up when the shade is fully expanded to automatically
-     * collapse once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully expanded once the user stops swiping.
-     */
-    val swipeCollapseThreshold: StateFlow<Float> =
-        interactor.shadeConfig
-            .map { it.swipeCollapseThreshold }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
-            )
-
-    /**
-     * The amount that the user must swipe down when the shade is fully collapsed to automatically
-     * expand once the user lets go of the shade. If the user swipes less than this amount, the
-     * shade will automatically revert back to fully collapsed once the user stops swiping.
-     */
-    val swipeExpandThreshold: StateFlow<Float> =
-        interactor.shadeConfig
-            .map { it.swipeExpandThreshold }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
-            )
-
-    /**
-     * Proxied input affecting the shade. This is input coming from sources outside of system UI
-     * (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
-     * shade (for example, the scrim that's shown behind the shades).
-     */
-    val proxiedInput: Flow<ProxiedInputModel?> =
-        interactor
-            .proxiedInput(shadeId)
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = null,
-            )
-
-    /** Notifies that the expansion amount for the shade has changed. */
-    fun onExpansionChanged(
-        expansion: Float,
-    ) {
-        interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
-    }
-
-    /** Notifies that a drag gesture has started. */
-    fun onDragStarted() {
-        interactor.onUserInteractionStarted(shadeId)
-    }
-
-    /** Notifies that a drag gesture has ended. */
-    fun onDragEnded() {
-        interactor.onUserInteractionEnded(shadeId = shadeId)
-    }
-
-    private fun shadeWidth(shadeConfig: ShadeConfig): Size {
-        return when (shadeId) {
-            ShadeId.LEFT ->
-                Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
-            ShadeId.RIGHT ->
-                Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
-            ShadeId.SINGLE -> Size.Fraction(1f)
-        }
-    }
-
-    sealed class Size {
-        data class Fraction(
-            @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
-        ) : Size()
-        data class Pixels(
-            val pixels: Int,
-        ) : Size()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 682335e..e134f7c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -133,6 +133,7 @@
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.rotation.RotationButton;
@@ -199,6 +200,7 @@
     private final SysUiState mSysUiFlagsContainer;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
     private final ShadeController mShadeController;
+    private final ShadeViewController mShadeViewController;
     private final NotificationRemoteInputManager mNotificationRemoteInputManager;
     private final OverviewProxyService mOverviewProxyService;
     private final NavigationModeController mNavigationModeController;
@@ -523,6 +525,7 @@
     @Inject
     NavigationBar(
             NavigationBarView navigationBarView,
+            ShadeController shadeController,
             NavigationBarFrame navigationBarFrame,
             @Nullable Bundle savedState,
             @DisplayId Context context,
@@ -541,7 +544,7 @@
             Optional<Pip> pipOptional,
             Optional<Recents> recentsOptional,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
-            ShadeController shadeController,
+            ShadeViewController shadeViewController,
             NotificationRemoteInputManager notificationRemoteInputManager,
             NotificationShadeDepthController notificationShadeDepthController,
             @Main Handler mainHandler,
@@ -577,6 +580,7 @@
         mSysUiFlagsContainer = sysUiFlagsContainer;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
         mShadeController = shadeController;
+        mShadeViewController = shadeViewController;
         mNotificationRemoteInputManager = notificationRemoteInputManager;
         mOverviewProxyService = overviewProxyService;
         mNavigationModeController = navigationModeController;
@@ -739,8 +743,7 @@
         final Display display = mView.getDisplay();
         mView.setComponents(mRecentsOptional);
         if (mCentralSurfacesOptionalLazy.get().isPresent()) {
-            mView.setComponents(
-                    mCentralSurfacesOptionalLazy.get().get().getShadeViewController());
+            mView.setComponents(mShadeViewController);
         }
         mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
         mView.setOnVerticalChangedListener(this::onVerticalChanged);
@@ -1341,9 +1344,10 @@
     }
 
     private void onVerticalChanged(boolean isVertical) {
-        Optional<CentralSurfaces> cs = mCentralSurfacesOptionalLazy.get();
-        if (cs.isPresent() && cs.get().getShadeViewController() != null) {
-            cs.get().getShadeViewController().setQsScrimEnabled(!isVertical);
+        // This check can probably be safely removed. It only remained to reduce regression
+        // risk for a broad change that removed the CentralSurfaces reference in the if block
+        if (mCentralSurfacesOptionalLazy.get().isPresent()) {
+            mShadeViewController.setQsScrimEnabled(!isVertical);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 207cc139..a737a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -78,7 +78,6 @@
 import com.android.internal.app.AssistUtils;
 import com.android.internal.app.IVoiceInteractionSessionListener;
 import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.util.ScreenshotHelper;
 import com.android.internal.util.ScreenshotRequest;
 import com.android.systemui.Dumpable;
@@ -143,6 +142,7 @@
     private final Executor mMainExecutor;
     private final ShellInterface mShellInterface;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
+    private final Lazy<ShadeViewController> mShadeViewControllerLazy;
     private SysUiState mSysUiState;
     private final Handler mHandler;
     private final Lazy<NavigationBarController> mNavBarControllerLazy;
@@ -201,11 +201,7 @@
                 // TODO move this logic to message queue
                 mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
                     if (event.getActionMasked() == ACTION_DOWN) {
-                        ShadeViewController shadeViewController =
-                                centralSurfaces.getShadeViewController();
-                        if (shadeViewController != null) {
-                            shadeViewController.startExpandLatencyTracking();
-                        }
+                        mShadeViewControllerLazy.get().startExpandLatencyTracking();
                     }
                     mHandler.post(() -> {
                         int action = event.getActionMasked();
@@ -552,8 +548,10 @@
             ShellInterface shellInterface,
             Lazy<NavigationBarController> navBarControllerLazy,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+            Lazy<ShadeViewController> shadeViewControllerLazy,
             NavigationModeController navModeController,
-            NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
+            NotificationShadeWindowController statusBarWinController,
+            SysUiState sysUiState,
             UserTracker userTracker,
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
@@ -573,6 +571,7 @@
         mMainExecutor = mainExecutor;
         mShellInterface = shellInterface;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
+        mShadeViewControllerLazy = shadeViewControllerLazy;
         mHandler = new Handler();
         mNavBarControllerLazy = navBarControllerLazy;
         mStatusBarWinController = statusBarWinController;
@@ -677,13 +676,10 @@
                 mNavBarControllerLazy.get().getDefaultNavigationBar();
         final NavigationBarView navBarView =
                 mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
-        final ShadeViewController panelController =
-                mCentralSurfacesOptionalLazy.get()
-                        .map(CentralSurfaces::getShadeViewController)
-                        .orElse(null);
         if (SysUiState.DEBUG) {
             Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
-                    + " navBarView=" + navBarView + " panelController=" + panelController);
+                    + " navBarView=" + navBarView
+                    + " shadeViewController=" + mShadeViewControllerLazy.get());
         }
 
         if (navBarFragment != null) {
@@ -692,9 +688,7 @@
         if (navBarView != null) {
             navBarView.updateDisabledSystemUiStateFlags(mSysUiState);
         }
-        if (panelController != null) {
-            panelController.updateSystemUiStateFlags();
-        }
+        mShadeViewControllerLazy.get().updateSystemUiStateFlags();
         if (mStatusBarWinController != null) {
             mStatusBarWinController.notifyStateChangedCallbacks();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index b23c4ec..92384d6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -20,14 +20,22 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.DisplayId
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
+import com.android.systemui.model.updateFlags
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneContainerNames
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -49,12 +57,15 @@
     private val authenticationInteractor: AuthenticationInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     private val featureFlags: FeatureFlags,
+    private val sysUiState: SysUiState,
+    @DisplayId private val displayId: Int,
 ) : CoreStartable {
 
     override fun start() {
         if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
             hydrateVisibility()
             automaticallySwitchScenes()
+            hydrateSystemUiState()
         }
     }
 
@@ -121,6 +132,27 @@
         }
     }
 
+    /** Keeps [SysUiState] up-to-date */
+    private fun hydrateSystemUiState() {
+        applicationScope.launch {
+            sceneInteractor
+                .currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT)
+                .map { it.key }
+                .distinctUntilChanged()
+                .collect { sceneKey ->
+                    sysUiState.updateFlags(
+                        displayId,
+                        SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+                        SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+                        SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+                        SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+                        SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
+                            (sceneKey == SceneKey.Lockscreen),
+                    )
+                }
+        }
+    }
+
     private fun switchToScene(targetSceneKey: SceneKey) {
         sceneInteractor.setCurrentScene(
             containerName = CONTAINER_NAME,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 202d6e6..e428976 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -147,7 +147,6 @@
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -365,7 +364,6 @@
     private KeyguardBottomAreaView mKeyguardBottomArea;
     private boolean mExpanding;
     private boolean mSplitShadeEnabled;
-    private boolean mDualShadeEnabled;
     /** The bottom padding reserved for elements of the keyguard measuring notifications. */
     private float mKeyguardNotificationBottomPadding;
     /**
@@ -599,7 +597,6 @@
     private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private final KeyguardInteractor mKeyguardInteractor;
     private final KeyguardViewConfigurator mKeyguardViewConfigurator;
-    private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
     private final CoroutineDispatcher mMainDispatcher;
     private boolean mIsAnyMultiShadeExpanded;
     private boolean mIsOcclusionTransitionRunning = false;
@@ -735,7 +732,6 @@
             LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
             @Main CoroutineDispatcher mainDispatcher,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
-            Provider<MultiShadeInteractor> multiShadeInteractorProvider,
             DumpManager dumpManager,
             KeyguardLongPressViewModel keyguardLongPressViewModel,
             KeyguardInteractor keyguardInteractor,
@@ -839,8 +835,6 @@
         mFeatureFlags = featureFlags;
         mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
         mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
-        mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
-        mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null;
         mFalsingCollector = falsingCollector;
         mPowerManager = powerManager;
         mWakeUpCoordinator = coordinator;
@@ -1079,11 +1073,6 @@
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
 
-        if (mDualShadeEnabled) {
-            collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(),
-                    mMultiShadeExpansionConsumer, mMainDispatcher);
-        }
-
         // Dreaming->Lockscreen
         collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
                 mDreamingToLockscreenTransition, mMainDispatcher);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 108ea68..6afed1d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -31,9 +31,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewStub;
-
-import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
@@ -46,7 +43,6 @@
 import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -57,9 +53,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.log.BouncerLogger;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
-import com.android.systemui.multishade.ui.view.MultiShadeView;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -83,7 +76,6 @@
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 /**
  * Controller for {@link NotificationShadeWindowView}.
@@ -135,7 +127,6 @@
                     step.getTransitionState() == TransitionState.RUNNING;
             };
     private final SystemClock mClock;
-    private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
 
     @Inject
     public NotificationShadeWindowViewController(
@@ -167,9 +158,7 @@
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
             FeatureFlags featureFlags,
-            Provider<MultiShadeInteractor> multiShadeInteractorProvider,
             SystemClock clock,
-            Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
             BouncerMessageInteractor bouncerMessageInteractor,
             BouncerLogger bouncerLogger) {
         mLockscreenShadeTransitionController = transitionController;
@@ -219,17 +208,6 @@
                     progressProvider -> progressProvider.addCallback(
                             mDisableSubpixelTextTransitionListener));
         }
-        if (ComposeFacade.INSTANCE.isComposeAvailable()
-                && featureFlags.isEnabled(Flags.DUAL_SHADE)) {
-            mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get();
-            final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub);
-            if (multiShadeViewStub != null) {
-                final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate();
-                multiShadeView.init(multiShadeInteractorProvider.get(), clock);
-            }
-        } else {
-            mMultiShadeMotionEventInteractor = null;
-        }
     }
 
     /**
@@ -395,10 +373,7 @@
                     return true;
                 }
 
-                if (mMultiShadeMotionEventInteractor != null) {
-                    // This interactor is not null only if the dual shade feature is enabled.
-                    return mMultiShadeMotionEventInteractor.shouldIntercept(ev);
-                } else if (mNotificationPanelViewController.isFullyExpanded()
+                if (mNotificationPanelViewController.isFullyExpanded()
                         && mDragDownHelper.isDragDownEnabled()
                         && !mService.isBouncerShowing()
                         && !mStatusBarStateController.isDozing()) {
@@ -428,10 +403,7 @@
                     return true;
                 }
 
-                if (mMultiShadeMotionEventInteractor != null) {
-                    // This interactor is not null only if the dual shade feature is enabled.
-                    return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth());
-                } else if (mDragDownHelper.isDragDownEnabled()
+                if (mDragDownHelper.isDragDownEnabled()
                         || mDragDownHelper.isDraggingDown()) {
                     // we still want to finish our drag down gesture when locking the screen
                     return mDragDownHelper.onTouchEvent(ev) || handled;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index ee4e98e..fe4832f0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade
 
+import android.graphics.Point
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.PowerManager
 import android.provider.Settings
@@ -25,6 +26,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -52,6 +54,7 @@
         private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
         private val statusBarStateController: StatusBarStateController,
         private val shadeLogger: ShadeLogger,
+        private val dozeInteractor: DozeInteractor,
         userTracker: UserTracker,
         tunerService: TunerService,
         dumpManager: DumpManager
@@ -86,6 +89,7 @@
             shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
             if (proximityIsNotNear && isNotAFalseTap) {
                 shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
+                dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt()))
                 powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
             }
             return true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 324e972..645595c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -23,6 +23,7 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.view.View;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -151,4 +152,20 @@
                 BIOMETRIC_ERROR_VIBRATION_EFFECT, reason,
                 HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
     }
+
+    /**
+     * Perform a vibration using a view and the one-way API with flags
+     * @see View#performHapticFeedback(int feedbackConstant, int flags)
+     */
+    public void performHapticFeedback(@NonNull View view, int feedbackConstant, int flags) {
+        view.performHapticFeedback(feedbackConstant, flags);
+    }
+
+    /**
+     * Perform a vibration using a view and the one-way API
+     * @see View#performHapticFeedback(int feedbackConstant)
+     */
+    public void performHapticFeedback(@NonNull View view, int feedbackConstant) {
+        view.performHapticFeedback(feedbackConstant);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 42b99a1..ed489a6c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3678,6 +3678,7 @@
             pw.print(", mShowingPublicInitialized: " + mShowingPublicInitialized);
             NotificationContentView showingLayout = getShowingLayout();
             pw.print(", privateShowing: " + (showingLayout == mPrivateLayout));
+            pw.print(", mShowNoBackground: " + mShowNoBackground);
             pw.println();
             showingLayout.dump(pw, args);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 26b51a9..dcd18dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -69,6 +70,7 @@
     private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
     private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
     private val shadeControllerLazy: Lazy<ShadeController>,
+    private val shadeViewControllerLazy: Lazy<ShadeViewController>,
     private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
     private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
@@ -896,7 +898,7 @@
                 if (dismissShade) {
                     return StatusBarLaunchAnimatorController(
                         animationController,
-                        it.shadeViewController,
+                        shadeViewControllerLazy.get(),
                         shadeControllerLazy.get(),
                         notifShadeWindowControllerLazy.get(),
                         isLaunchForActivity
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index dc021fe..661d71d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -160,7 +160,6 @@
     private KeyguardViewController mKeyguardViewController;
     private DozeScrimController mDozeScrimController;
     private KeyguardViewMediator mKeyguardViewMediator;
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private PendingAuthenticated mPendingAuthenticated = null;
     private boolean mHasScreenTurnedOnSinceAuthenticating;
     private boolean mFadedAwayAfterWakeAndUnlock;
@@ -281,8 +280,7 @@
             LatencyTracker latencyTracker,
             ScreenOffAnimationController screenOffAnimationController,
             VibratorHelper vibrator,
-            SystemClock systemClock,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager
+            SystemClock systemClock
     ) {
         mPowerManager = powerManager;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -310,7 +308,6 @@
         mVibratorHelper = vibrator;
         mLogger = biometricUnlockLogger;
         mSystemClock = systemClock;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
 
         dumpManager.registerDumpable(this);
     }
@@ -452,19 +449,8 @@
         // During wake and unlock, we need to draw black before waking up to avoid abrupt
         // brightness changes due to display state transitions.
         Runnable wakeUp = ()-> {
-            // Check to see if we are still locked when we are waking and unlocking from dream.
-            // This runnable should be executed after unlock. If that's true, we could be not
-            // dreaming, but still locked. In this case, we should attempt to authenticate instead
-            // of waking up.
-            if (mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
-                    && !mKeyguardStateController.isUnlocked()
-                    && !mUpdateMonitor.isDreaming()) {
-                // Post wakeUp runnable is called from a callback in keyguard.
-                mHandler.post(() -> mKeyguardViewController.notifyKeyguardAuthenticated(
-                        false /* primaryAuth */));
-            } else if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
+            if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
                 mLogger.i("bio wakelock: Authenticated, waking up...");
-
                 mPowerManager.wakeUp(
                         mSystemClock.uptimeMillis(),
                         PowerManager.WAKE_REASON_BIOMETRIC,
@@ -476,7 +462,10 @@
             Trace.endSection();
         };
 
-        if (mMode != MODE_NONE && mMode != MODE_WAKE_AND_UNLOCK_FROM_DREAM) {
+        final boolean wakingFromDream = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
+                && !mStatusBarStateController.isDozing();
+
+        if (mMode != MODE_NONE && !wakingFromDream) {
             wakeUp.run();
         }
         switch (mMode) {
@@ -498,10 +487,6 @@
                 Trace.endSection();
                 break;
             case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
-                // In the case of waking and unlocking from dream, waking up is delayed until after
-                // unlock is complete to avoid conflicts during each sequence's transitions.
-                mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(wakeUp);
-                // Execution falls through here to proceed unlocking.
             case MODE_WAKE_AND_UNLOCK_PULSING:
             case MODE_WAKE_AND_UNLOCK:
                 if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) {
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 acd6e49..5c28be3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -44,7 +44,6 @@
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.qs.QSPanelController;
-import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -195,9 +194,6 @@
     @Override
     Lifecycle getLifecycle();
 
-    /** */
-    ShadeViewController getShadeViewController();
-
     /** Get the Keyguard Message Area that displays auth messages. */
     AuthKeyguardMessageArea getKeyguardMessageArea();
 
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 c3c9a61..6eeb25f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1326,7 +1326,7 @@
             }
         });
 
-        mScreenOffAnimationController.initialize(this, mLightRevealScrim);
+        mScreenOffAnimationController.initialize(this, mShadeSurface, mLightRevealScrim);
         updateLightRevealScrimVisibility();
 
         mShadeSurface.initDependencies(
@@ -1677,8 +1677,7 @@
         Trace.endSection();
     }
 
-    @Override
-    public ShadeViewController getShadeViewController() {
+    protected ShadeViewController getShadeViewController() {
         return mShadeSurface;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
index c817466..89c3a02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
@@ -18,6 +18,7 @@
 import android.view.View
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.unfold.FoldAodAnimationController
 import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -37,8 +38,12 @@
     private val animations: List<ScreenOffAnimation> =
         listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation)
 
-    fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
-        animations.forEach { it.initialize(centralSurfaces, lightRevealScrim) }
+    fun initialize(
+            centralSurfaces: CentralSurfaces,
+            shadeViewController: ShadeViewController,
+            lightRevealScrim: LightRevealScrim,
+    ) {
+        animations.forEach { it.initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
         wakefulnessLifecycle.addObserver(this)
     }
 
@@ -197,7 +202,11 @@
 }
 
 interface ScreenOffAnimation {
-    fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {}
+    fun initialize(
+            centralSurfaces: CentralSurfaces,
+            shadeViewController: ShadeViewController,
+            lightRevealScrim: LightRevealScrim,
+    ) {}
 
     /**
      * Called when started going to sleep, should return true if the animation will be played
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 96a4d90..7e9172d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -66,7 +67,8 @@
     private val powerManager: PowerManager,
     private val handler: Handler = Handler()
 ) : WakefulnessLifecycle.Observer, ScreenOffAnimation {
-    private lateinit var mCentralSurfaces: CentralSurfaces
+    private lateinit var centralSurfaces: CentralSurfaces
+    private lateinit var shadeViewController: ShadeViewController
     /**
      * Whether or not [initialize] has been called to provide us with the StatusBar,
      * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen
@@ -126,7 +128,7 @@
         lightRevealAnimator.start()
     }
 
-    val animatorDurationScaleObserver = object : ContentObserver(null) {
+    private val animatorDurationScaleObserver = object : ContentObserver(null) {
         override fun onChange(selfChange: Boolean) {
             updateAnimatorDurationScale()
         }
@@ -134,11 +136,13 @@
 
     override fun initialize(
         centralSurfaces: CentralSurfaces,
+        shadeViewController: ShadeViewController,
         lightRevealScrim: LightRevealScrim
     ) {
         this.initialized = true
         this.lightRevealScrim = lightRevealScrim
-        this.mCentralSurfaces = centralSurfaces
+        this.centralSurfaces = centralSurfaces
+        this.shadeViewController = shadeViewController
 
         updateAnimatorDurationScale()
         globalSettings.registerContentObserver(
@@ -198,7 +202,7 @@
 
                     // Tell the CentralSurfaces to become keyguard for real - we waited on that
                     // since it is slow and would have caused the animation to jank.
-                    mCentralSurfaces.updateIsKeyguard()
+                    centralSurfaces.updateIsKeyguard()
 
                     // Run the callback given to us by the KeyguardVisibilityHelper.
                     after.run()
@@ -251,7 +255,7 @@
             // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
             // changed parts of the UI (such as showing AOD in the shade) without actually changing
             // the StatusBarState. This ensures that the UI definitely reflects the desired state.
-            mCentralSurfaces.updateIsKeyguard(true /* forceStateChange */)
+            centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
         }
     }
 
@@ -280,7 +284,7 @@
 
                     // Show AOD. That'll cause the KeyguardVisibilityHelper to call
                     // #animateInKeyguard.
-                    mCentralSurfaces.shadeViewController.showAodUi()
+                    shadeViewController.showAodUi()
                 }
             }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
 
@@ -328,8 +332,8 @@
         // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
         // already expanded and showing notifications/QS, the animation looks really messy. For now,
         // disable it if the notification panel is expanded.
-        if ((!this::mCentralSurfaces.isInitialized ||
-                mCentralSurfaces.shadeViewController.isPanelExpanded) &&
+        if ((!this::centralSurfaces.isInitialized ||
+                shadeViewController.isPanelExpanded) &&
                 // Status bar might be expanded because we have started
                 // playing the animation already
                 !isAnimationPlaying()
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index cbe4020..098d51e 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.shade.ShadeFoldAnimator
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.phone.ScreenOffAnimation
@@ -62,7 +63,7 @@
     private val keyguardInteractor: Lazy<KeyguardInteractor>,
 ) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer {
 
-    private lateinit var centralSurfaces: CentralSurfaces
+    private lateinit var shadeViewController: ShadeViewController
 
     private var isFolded = false
     private var isFoldHandled = true
@@ -87,8 +88,12 @@
         )
     }
 
-    override fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
-        this.centralSurfaces = centralSurfaces
+    override fun initialize(
+            centralSurfaces: CentralSurfaces,
+            shadeViewController: ShadeViewController,
+            lightRevealScrim: LightRevealScrim,
+    ) {
+        this.shadeViewController = shadeViewController
 
         deviceStateManager.registerCallback(mainExecutor, FoldListener())
         wakefulnessLifecycle.addObserver(this)
@@ -128,7 +133,7 @@
     }
 
     private fun getShadeFoldAnimator(): ShadeFoldAnimator =
-        centralSurfaces.shadeViewController.shadeFoldAnimator
+        shadeViewController.shadeFoldAnimator
 
     private fun setAnimationState(playing: Boolean) {
         shouldPlayAnimation = playing
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index d447174..3abae6b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -37,6 +37,7 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate
 import com.android.systemui.biometrics.SideFpsController
 import com.android.systemui.biometrics.SideFpsUiRequestSource
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
@@ -123,6 +124,7 @@
     @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
     @Mock private lateinit var audioManager: AudioManager
     @Mock private lateinit var userInteractor: UserInteractor
+    @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
 
     @Captor
     private lateinit var swipeListenerArgumentCaptor:
@@ -224,6 +226,7 @@
                 mock(),
                 { JavaAdapter(sceneTestUtils.testScope.backgroundScope) },
                 userInteractor,
+                faceAuthAccessibilityDelegate,
             ) {
                 sceneInteractor
             }
@@ -244,6 +247,11 @@
     }
 
     @Test
+    fun setAccessibilityDelegate() {
+        verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate)
+    }
+
+    @Test
     fun showSecurityScreen_canInflateAllModes() {
         val modes = SecurityMode.values()
         for (mode in modes) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
index 025c88c..576f689 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
@@ -39,6 +39,7 @@
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
@@ -65,6 +66,8 @@
     @Mock
     private ShadeController mShadeController;
     @Mock
+    private ShadeViewController mShadeViewController;
+    @Mock
     private Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
     @Mock
     private Optional<Recents> mRecentsOptional;
@@ -82,7 +85,8 @@
         mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
         mContext.addMockSystemService(InputManager.class, mInputManager);
         mSystemActions = new SystemActions(mContext, mUserTracker, mNotificationShadeController,
-                mShadeController, mCentralSurfacesOptionalLazy, mRecentsOptional, mDisplayTracker);
+                mShadeController, () -> mShadeViewController, mCentralSurfacesOptionalLazy,
+                mRecentsOptional, mDisplayTracker);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
index d5e6881..7b99314 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
@@ -15,11 +15,13 @@
  */
 package com.android.systemui.accessibility.fontscaling
 
+import android.content.res.Configuration
 import android.os.Handler
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.ViewGroup
+import android.widget.Button
 import android.widget.SeekBar
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -61,6 +63,7 @@
     private lateinit var secureSettings: SecureSettings
     private lateinit var systemClock: FakeSystemClock
     private lateinit var backgroundDelayableExecutor: FakeExecutor
+    private lateinit var testableLooper: TestableLooper
     private val fontSizeValueArray: Array<String> =
         mContext
             .getResources()
@@ -73,7 +76,8 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        val mainHandler = Handler(TestableLooper.get(this).getLooper())
+        testableLooper = TestableLooper.get(this)
+        val mainHandler = Handler(testableLooper.looper)
         systemSettings = FakeSettings()
         // Guarantee that the systemSettings always starts with the default font scale.
         systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
@@ -286,4 +290,26 @@
         verify(fontScalingDialog).createTextPreview(/* index= */ 0)
         fontScalingDialog.dismiss()
     }
+
+    @Test
+    fun changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishes() {
+        fontScalingDialog.show()
+
+        val iconEndFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_end_frame)!!
+        val doneButton: Button = fontScalingDialog.findViewById(com.android.internal.R.id.button1)!!
+
+        iconEndFrame.performClick()
+        backgroundDelayableExecutor.runAllReady()
+        backgroundDelayableExecutor.advanceClockToNext()
+        backgroundDelayableExecutor.runAllReady()
+
+        // Verify that the button is disabled before receiving onConfigurationChanged
+        assertThat(doneButton.isEnabled).isFalse()
+
+        val config = Configuration()
+        config.fontScale = 1.15f
+        fontScalingDialog.onConfigurationChanged(config)
+        testableLooper.processAllMessages()
+        assertThat(doneButton.isEnabled).isTrue()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
new file mode 100644
index 0000000..0056970
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.authentication.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationRepositoryTest : SysuiTestCase() {
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+    private val testUtils = SceneTestUtils(this)
+    private val testScope = testUtils.testScope
+    private val userRepository = FakeUserRepository()
+
+    private lateinit var underTest: AuthenticationRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        userRepository.setUserInfos(USER_INFOS)
+        runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
+
+        underTest =
+            AuthenticationRepositoryImpl(
+                applicationScope = testScope.backgroundScope,
+                getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN },
+                backgroundDispatcher = testUtils.testDispatcher,
+                userRepository = userRepository,
+                keyguardRepository = testUtils.keyguardRepository,
+                lockPatternUtils = lockPatternUtils,
+            )
+    }
+
+    @Test
+    fun isAutoConfirmEnabled() =
+        testScope.runTest {
+            whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true)
+            whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false)
+
+            val values by collectValues(underTest.isAutoConfirmEnabled)
+            assertThat(values.first()).isFalse()
+            assertThat(values.last()).isTrue()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+            assertThat(values.last()).isFalse()
+        }
+
+    @Test
+    fun isPatternVisible() =
+        testScope.runTest {
+            whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false)
+            whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true)
+
+            val values by collectValues(underTest.isPatternVisible)
+            assertThat(values.first()).isTrue()
+            assertThat(values.last()).isFalse()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+            assertThat(values.last()).isTrue()
+        }
+
+    companion object {
+        private val USER_INFOS =
+            listOf(
+                UserInfo(
+                    /* id= */ 100,
+                    /* name= */ "First user",
+                    /* flags= */ 0,
+                ),
+                UserInfo(
+                    /* id= */ 101,
+                    /* name= */ "Second user",
+                    /* flags= */ 0,
+                ),
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
new file mode 100644
index 0000000..ec17794
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.testing.TestableLooper
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class FaceAuthAccessibilityDelegateTest : SysuiTestCase() {
+
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var hostView: View
+    @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+    private lateinit var underTest: FaceAuthAccessibilityDelegate
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            FaceAuthAccessibilityDelegate(
+                context.resources,
+                keyguardUpdateMonitor,
+                faceAuthInteractor,
+            )
+    }
+
+    @Test
+    fun shouldListenForFaceTrue_onInitializeAccessibilityNodeInfo_clickActionAdded() {
+        whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+        // WHEN node is initialized
+        val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+        underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+        // THEN a11y action is added
+        val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>()
+        verify(mockedNodeInfo).addAction(argumentCaptor.capture())
+
+        // AND the a11y action is a click action
+        assertEquals(
+            AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+            argumentCaptor.value.id
+        )
+    }
+
+    @Test
+    fun shouldListenForFaceFalse_onInitializeAccessibilityNodeInfo_clickActionNotAdded() {
+        whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false)
+
+        // WHEN node is initialized
+        val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+        underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+        // THEN a11y action is NOT added
+        verify(mockedNodeInfo, never())
+            .addAction(any(AccessibilityNodeInfo.AccessibilityAction::class.java))
+    }
+
+    @Test
+    fun performAccessibilityAction_actionClick_retriesFaceAuth() {
+        whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+        // WHEN click action is performed
+        underTest.performAccessibilityAction(
+            hostView,
+            AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+            null
+        )
+
+        // THEN retry face auth
+        verify(keyguardUpdateMonitor)
+            .requestFaceAuth(eq(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION))
+        verify(faceAuthInteractor).onAccessibilityAction()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index ecc776b..3169b09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -44,6 +44,9 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
 import android.view.WindowMetrics
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -53,14 +56,9 @@
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dump.DumpManager
@@ -101,7 +99,7 @@
 @SmallTest
 @RoboPilotTest
 @RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
 class SideFpsControllerTest : SysuiTestCase() {
 
     @JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@
     private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
     private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
     private lateinit var displayStateInteractor: DisplayStateInteractor
-    private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
-    private val fingerprintRepository = FakeFingerprintPropertyRepository()
 
     private val executor = FakeExecutor(FakeSystemClock())
     private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -163,15 +159,6 @@
                 executor,
                 rearDisplayStateRepository
             )
-        sideFpsOverlayViewModel =
-            SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository))
-
-        fingerprintRepository.setProperties(
-            sensorId = 1,
-            strength = SensorStrength.STRONG,
-            sensorType = FingerprintSensorType.REAR,
-            sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0))
-        )
 
         context.addMockSystemService(DisplayManager::class.java, displayManager)
         context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@
                 executor,
                 handler,
                 alternateBouncerInteractor,
-                { sideFpsOverlayViewModel },
                 TestCoroutineScope(),
                 dumpManager
             )
@@ -697,6 +683,106 @@
         verify(windowManager).removeView(any())
     }
 
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+     * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+     * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+     * in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForXAlignedSensor_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+        }
+
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+     * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+     * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+     * correctly, tests for indicator placement in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+        }
+
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+     * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+     * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+     * in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = false,
+            { rotation = Surface.ROTATION_0 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+        }
+
+    /**
+     * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+     * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+     * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+     * correctly, tests for indicator placement in other rotations have been omitted.
+     */
+    @Test
+    fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED,
+            isReverseDefaultRotation = true,
+            { rotation = Surface.ROTATION_270 }
+        ) {
+            sideFpsController.overlayOffsets = sensorLocation
+
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+        }
+
     @Test
     fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
         // By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@
 
         assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
     }
+
+    @Test
+    fun testLayoutParams_isKeyguardDialogType() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            val lpType = overlayViewParamsCaptor.value.type
+
+            assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
+        }
+
+    @Test
+    fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+            assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+        }
+
+    @Test
+    fun testLayoutParams_hasTrustedOverlayWindowFlag() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+            assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+        }
 }
 
 private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index ea25615..239e317 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -73,15 +73,10 @@
     @Test
     fun initializeProperties() =
         testScope.runTest {
-            val sensorId by collectLastValue(repository.sensorId)
-            val strength by collectLastValue(repository.strength)
-            val sensorType by collectLastValue(repository.sensorType)
-            val sensorLocations by collectLastValue(repository.sensorLocations)
+            val isInitialized = collectLastValue(repository.isInitialized)
 
-            // Assert default properties.
-            assertThat(sensorId).isEqualTo(-1)
-            assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
-            assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
+            assertDefaultProperties()
+            assertThat(isInitialized()).isFalse()
 
             val fingerprintProps =
                 listOf(
@@ -120,24 +115,31 @@
 
             fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
 
-            assertThat(sensorId).isEqualTo(1)
-            assertThat(strength).isEqualTo(SensorStrength.STRONG)
-            assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
+            assertThat(repository.sensorId.value).isEqualTo(1)
+            assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
+            assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
 
-            assertThat(sensorLocations?.size).isEqualTo(2)
-            assertThat(sensorLocations).containsKey("display_id_1")
-            with(sensorLocations?.get("display_id_1")!!) {
+            assertThat(repository.sensorLocations.value.size).isEqualTo(2)
+            assertThat(repository.sensorLocations.value).containsKey("display_id_1")
+            with(repository.sensorLocations.value["display_id_1"]!!) {
                 assertThat(displayId).isEqualTo("display_id_1")
                 assertThat(sensorLocationX).isEqualTo(100)
                 assertThat(sensorLocationY).isEqualTo(300)
                 assertThat(sensorRadius).isEqualTo(20)
             }
-            assertThat(sensorLocations).containsKey("")
-            with(sensorLocations?.get("")!!) {
+            assertThat(repository.sensorLocations.value).containsKey("")
+            with(repository.sensorLocations.value[""]!!) {
                 assertThat(displayId).isEqualTo("")
                 assertThat(sensorLocationX).isEqualTo(540)
                 assertThat(sensorLocationY).isEqualTo(1636)
                 assertThat(sensorRadius).isEqualTo(130)
             }
+            assertThat(isInitialized()).isTrue()
         }
+
+    private fun assertDefaultProperties() {
+        assertThat(repository.sensorId.value).isEqualTo(-1)
+        assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
+        assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
index 896f9b11..fd96cf4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@
     }
 
     @Test
-    fun testGetOverlayoffsets() =
+    fun testGetOverlayOffsets() =
         testScope.runTest {
-            // Arrange.
             fingerprintRepository.setProperties(
                 sensorId = 1,
                 strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@
                     )
             )
 
-            // Act.
-            val offsets by collectLastValue(interactor.overlayOffsets)
-            val displayId by collectLastValue(interactor.displayId)
+            var offsets = interactor.getOverlayOffsets("display_id_1")
+            assertThat(offsets.displayId).isEqualTo("display_id_1")
+            assertThat(offsets.sensorLocationX).isEqualTo(100)
+            assertThat(offsets.sensorLocationY).isEqualTo(300)
+            assertThat(offsets.sensorRadius).isEqualTo(20)
 
-            // Assert offsets of empty displayId.
-            assertThat(displayId).isEqualTo("")
-            assertThat(offsets?.displayId).isEqualTo("")
-            assertThat(offsets?.sensorLocationX).isEqualTo(540)
-            assertThat(offsets?.sensorLocationY).isEqualTo(1636)
-            assertThat(offsets?.sensorRadius).isEqualTo(130)
-
-            // Offsets should be updated correctly.
-            interactor.changeDisplay("display_id_1")
-            assertThat(displayId).isEqualTo("display_id_1")
-            assertThat(offsets?.displayId).isEqualTo("display_id_1")
-            assertThat(offsets?.sensorLocationX).isEqualTo(100)
-            assertThat(offsets?.sensorLocationY).isEqualTo(300)
-            assertThat(offsets?.sensorRadius).isEqualTo(20)
-
-            // Should return default offset when the displayId is invalid.
-            interactor.changeDisplay("invalid_display_id")
-            assertThat(displayId).isEqualTo("invalid_display_id")
-            assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
-            assertThat(offsets?.sensorLocationX)
-                .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
-            assertThat(offsets?.sensorLocationY)
-                .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
-            assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
+            offsets = interactor.getOverlayOffsets("invalid_display_id")
+            assertThat(offsets.displayId).isEqualTo("")
+            assertThat(offsets.sensorLocationX).isEqualTo(540)
+            assertThat(offsets.sensorLocationY).isEqualTo(1636)
+            assertThat(offsets.sensorRadius).isEqualTo(130)
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
deleted file mode 100644
index a859321..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.viewmodel
-
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManagerGlobal
-import android.view.Display
-import android.view.DisplayAdjustments
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.junit.MockitoJUnit
-
-private const val DISPLAY_ID = 2
-
-@SmallTest
-@RunWith(JUnit4::class)
-class SideFpsOverlayViewModelTest : SysuiTestCase() {
-
-    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
-    private var testScope: TestScope = TestScope(StandardTestDispatcher())
-
-    private val fingerprintRepository = FakeFingerprintPropertyRepository()
-    private lateinit var interactor: SideFpsOverlayInteractor
-    private lateinit var viewModel: SideFpsOverlayViewModel
-
-    enum class DeviceConfig {
-        X_ALIGNED,
-        Y_ALIGNED,
-    }
-
-    private lateinit var deviceConfig: DeviceConfig
-    private lateinit var indicatorBounds: Rect
-    private lateinit var displayBounds: Rect
-    private lateinit var sensorLocation: SensorLocationInternal
-    private var displayWidth: Int = 0
-    private var displayHeight: Int = 0
-    private var boundsWidth: Int = 0
-    private var boundsHeight: Int = 0
-
-    @Before
-    fun setup() {
-        interactor = SideFpsOverlayInteractorImpl(fingerprintRepository)
-
-        fingerprintRepository.setProperties(
-            sensorId = 1,
-            strength = SensorStrength.STRONG,
-            sensorType = FingerprintSensorType.REAR,
-            sensorLocations =
-                mapOf(
-                    "" to
-                        SensorLocationInternal(
-                            "" /* displayId */,
-                            540 /* sensorLocationX */,
-                            1636 /* sensorLocationY */,
-                            130 /* sensorRadius */
-                        ),
-                    "display_id_1" to
-                        SensorLocationInternal(
-                            "display_id_1" /* displayId */,
-                            100 /* sensorLocationX */,
-                            300 /* sensorLocationY */,
-                            20 /* sensorRadius */
-                        )
-                )
-        )
-    }
-
-    @Test
-    fun testOverlayOffsets() =
-        testScope.runTest {
-            viewModel = SideFpsOverlayViewModel(mContext, interactor)
-
-            val interactorOffsets by collectLastValue(interactor.overlayOffsets)
-            val viewModelOffsets by collectLastValue(viewModel.overlayOffsets)
-
-            assertThat(viewModelOffsets).isEqualTo(interactorOffsets)
-        }
-
-    private fun testWithDisplay(
-        deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
-        isReverseDefaultRotation: Boolean = false,
-        initInfo: DisplayInfo.() -> Unit = {},
-        block: () -> Unit
-    ) {
-        this.deviceConfig = deviceConfig
-
-        when (deviceConfig) {
-            DeviceConfig.X_ALIGNED -> {
-                displayWidth = 3000
-                displayHeight = 1500
-                sensorLocation = SensorLocationInternal("", 2500, 0, 0)
-                boundsWidth = 200
-                boundsHeight = 100
-            }
-            DeviceConfig.Y_ALIGNED -> {
-                displayWidth = 2500
-                displayHeight = 2000
-                sensorLocation = SensorLocationInternal("", 0, 300, 0)
-                boundsWidth = 100
-                boundsHeight = 200
-            }
-        }
-
-        indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
-        displayBounds = Rect(0, 0, displayWidth, displayHeight)
-
-        val displayInfo = DisplayInfo()
-        displayInfo.initInfo()
-
-        val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java)
-        val display =
-            Display(
-                dmGlobal,
-                DISPLAY_ID,
-                displayInfo,
-                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
-            )
-
-        whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo)
-
-        val sideFpsOverlayViewModelContext =
-            context.createDisplayContext(display) as SysuiTestableContext
-        sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride(
-            com.android.internal.R.bool.config_reverseDefaultRotation,
-            isReverseDefaultRotation
-        )
-        viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor)
-
-        block()
-    }
-
-    /**
-     * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
-     * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
-     * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
-     * placement in other rotations have been omitted.
-     */
-    @Test
-    fun verifiesIndicatorPlacementForXAlignedSensor_0() =
-        testScope.runTest {
-            testWithDisplay(
-                deviceConfig = DeviceConfig.X_ALIGNED,
-                isReverseDefaultRotation = false,
-                { rotation = Surface.ROTATION_0 }
-            ) {
-                viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
-                val displayInfo: DisplayInfo = DisplayInfo()
-                context.display!!.getDisplayInfo(displayInfo)
-                assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0)
-
-                assertThat(viewModel.sensorBounds.value).isNotNull()
-                assertThat(viewModel.sensorBounds.value.left)
-                    .isEqualTo(sensorLocation.sensorLocationX)
-                assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
-            }
-        }
-
-    /**
-     * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
-     * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
-     * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
-     * works correctly, tests for indicator placement in other rotations have been omitted.
-     */
-    @Test
-    fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
-        testScope.runTest {
-            testWithDisplay(
-                deviceConfig = DeviceConfig.X_ALIGNED,
-                isReverseDefaultRotation = true,
-                { rotation = Surface.ROTATION_270 }
-            ) {
-                viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
-                assertThat(viewModel.sensorBounds.value).isNotNull()
-                assertThat(viewModel.sensorBounds.value.left)
-                    .isEqualTo(sensorLocation.sensorLocationX)
-                assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
-            }
-        }
-
-    /**
-     * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
-     * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
-     * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
-     * placement in other rotations have been omitted.
-     */
-    @Test
-    fun verifiesIndicatorPlacementForYAlignedSensor_0() =
-        testScope.runTest {
-            testWithDisplay(
-                deviceConfig = DeviceConfig.Y_ALIGNED,
-                isReverseDefaultRotation = false,
-                { rotation = Surface.ROTATION_0 }
-            ) {
-                viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
-                assertThat(viewModel.sensorBounds.value).isNotNull()
-                assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
-                assertThat(viewModel.sensorBounds.value.top)
-                    .isEqualTo(sensorLocation.sensorLocationY)
-            }
-        }
-
-    /**
-     * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
-     * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
-     * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
-     * works correctly, tests for indicator placement in other rotations have been omitted.
-     */
-    @Test
-    fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
-        testScope.runTest {
-            testWithDisplay(
-                deviceConfig = DeviceConfig.Y_ALIGNED,
-                isReverseDefaultRotation = true,
-                { rotation = Surface.ROTATION_270 }
-            ) {
-                viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
-                assertThat(viewModel.sensorBounds.value).isNotNull()
-                assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
-                assertThat(viewModel.sensorBounds.value.top)
-                    .isEqualTo(sensorLocation.sensorLocationY)
-            }
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
index 872c079..2b98214 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
@@ -61,10 +61,8 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces),
+        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
                 TOUCH_HEIGHT);
-        when(mCentralSurfaces.getShadeViewController())
-                .thenReturn(mShadeViewController);
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 00f67a3..1f66e5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -38,8 +38,10 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 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.eq;
 import static org.mockito.Mockito.inOrder;
@@ -386,6 +388,21 @@
         mViewMediator.setKeyguardEnabled(false);
         TestableLooper.get(this).processAllMessages();
 
+        mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+                mUpdateMonitor.getCurrentUser());
+        mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+        final ArgumentCaptor<Runnable> animationRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+                animationRunnableCaptor.capture());
+
+        when(mStatusBarStateController.isDreaming()).thenReturn(true);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        animationRunnableCaptor.getValue().run();
+
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        mViewMediator.mViewMediatorCallback.keyguardGone();
+
         // Then dream should wake up
         verify(mPowerManager).wakeUp(anyLong(), anyInt(),
                 eq("com.android.systemui:UNLOCK_DREAMING"));
@@ -705,6 +722,67 @@
     }
 
     @Test
+    public void testWakeAndUnlockingOverDream() {
+        // Send signal to wake
+        mViewMediator.onWakeAndUnlocking();
+
+        // Ensure not woken up yet
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+        // Verify keyguard told of authentication
+        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+        mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+                mUpdateMonitor.getCurrentUser());
+        mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+        final ArgumentCaptor<Runnable> animationRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+                animationRunnableCaptor.capture());
+
+        when(mStatusBarStateController.isDreaming()).thenReturn(true);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        animationRunnableCaptor.getValue().run();
+
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        mViewMediator.mViewMediatorCallback.keyguardGone();
+
+        // Verify woken up now.
+        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+    }
+
+    @Test
+    public void testWakeAndUnlockingOverDream_signalAuthenticateIfStillShowing() {
+        // Send signal to wake
+        mViewMediator.onWakeAndUnlocking();
+
+        // Ensure not woken up yet
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+        // Verify keyguard told of authentication
+        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+        clearInvocations(mStatusBarKeyguardViewManager);
+        mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+                mUpdateMonitor.getCurrentUser());
+        mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+        final ArgumentCaptor<Runnable> animationRunnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+                animationRunnableCaptor.capture());
+
+        when(mStatusBarStateController.isDreaming()).thenReturn(true);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        animationRunnableCaptor.getValue().run();
+
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+
+        mViewMediator.mViewMediatorCallback.keyguardGone();
+
+
+        // Verify keyguard view controller informed of authentication again
+        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+    }
+
+    @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void testDoKeyguardWhileInteractive_resets() {
         mViewMediator.setShowingLocked(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 8127ac6..b3f8000 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -265,7 +265,8 @@
             val successResult = successResult()
             authenticationCallback.value.onAuthenticationSucceeded(successResult)
 
-            assertThat(authStatus()).isEqualTo(SuccessFaceAuthenticationStatus(successResult))
+            val response = authStatus() as SuccessFaceAuthenticationStatus
+            assertThat(response.successResult).isEqualTo(successResult)
             assertThat(authenticated()).isTrue()
             assertThat(authRunning()).isFalse()
             assertThat(canFaceAuthRun()).isFalse()
@@ -494,7 +495,9 @@
             authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
             authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
 
-            assertThat(authStatus()).isEqualTo(HelpFaceAuthenticationStatus(9, "help msg"))
+            val response = authStatus() as HelpFaceAuthenticationStatus
+            assertThat(response.msg).isEqualTo("help msg")
+            assertThat(response.msgId).isEqualTo(response.msgId)
         }
 
     @Test
@@ -550,10 +553,8 @@
         }
 
     @Test
-    fun authenticateDoesNotRunWhenFpIsLockedOut() =
-        testScope.runTest {
-            testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) }
-        }
+    fun authenticateDoesNotRunWhenFaceIsDisabled() =
+        testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } }
 
     @Test
     fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() =
@@ -858,6 +859,19 @@
         }
 
     @Test
+    fun disableFaceUnlockLocksOutFaceUnlock() =
+        testScope.runTest {
+            runCurrent()
+            initCollectors()
+            assertThat(underTest.isLockedOut.value).isFalse()
+
+            underTest.lockoutFaceAuth()
+            runCurrent()
+
+            assertThat(underTest.isLockedOut.value).isTrue()
+        }
+
+    @Test
     fun detectDoesNotRunWhenFaceAuthNotSupportedInCurrentPosture() =
         testScope.runTest {
             testGatingCheckForDetect {
@@ -1070,10 +1084,11 @@
     }
 
     private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+        verify(faceManager, atLeastOnce())
+            .addLockoutResetCallback(faceLockoutResetCallback.capture())
         biometricSettingsRepository.setFaceEnrolled(true)
         biometricSettingsRepository.setIsFaceAuthEnabled(true)
         fakeUserRepository.setUserSwitching(false)
-        deviceEntryFingerprintAuthRepository.setLockedOut(false)
         trustRepository.setCurrentUserTrusted(false)
         keyguardRepository.setKeyguardGoingAway(false)
         keyguardRepository.setWakefulnessModel(
@@ -1087,6 +1102,7 @@
         biometricSettingsRepository.setIsUserInLockdown(false)
         fakeUserRepository.setSelectedUserInfo(primaryUser)
         biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)
+        faceLockoutResetCallback.value.onLockoutReset(0)
         bouncerRepository.setAlternateVisible(true)
         keyguardRepository.setKeyguardShowing(true)
         runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index f974577..ec30732 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -17,31 +17,43 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.graphics.Point
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.core.animation.AnimatorTestRule
 import androidx.test.filters.SmallTest
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealEffect
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RoboPilotTest
-@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
 class LightRevealScrimRepositoryTest : SysuiTestCase() {
     private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
     private lateinit var underTest: LightRevealScrimRepositoryImpl
 
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -50,112 +62,127 @@
     }
 
     @Test
-    fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() =
-        runTest {
-            val values = mutableListOf<LightRevealEffect>()
-            val job = launch { underTest.revealEffect.collect { values.add(it) } }
+    fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest {
+        val values = mutableListOf<LightRevealEffect>()
+        val job = launch { underTest.revealEffect.collect { values.add(it) } }
 
-            // We should initially emit the default reveal effect.
-            runCurrent()
-            values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
-
-            // The source and sensor locations are still null, so we should still be using the
-            // default reveal despite a biometric unlock.
-            fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
-
-            runCurrent()
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
+        fakeKeyguardRepository.setWakefulnessModel(
+            WakefulnessModel(
+                WakefulnessState.STARTING_TO_WAKE,
+                WakeSleepReason.OTHER,
+                WakeSleepReason.OTHER
             )
+        )
+        // We should initially emit the default reveal effect.
+        runCurrent()
+        values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
 
-            // We got a source but still have no sensor locations, so should be sticking with
-            // the default effect.
-            fakeKeyguardRepository.setBiometricUnlockSource(
-                BiometricUnlockSource.FINGERPRINT_SENSOR
-            )
+        // The source and sensor locations are still null, so we should still be using the
+        // default reveal despite a biometric unlock.
+        fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
 
-            runCurrent()
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
-            )
+        runCurrent()
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+        )
 
-            // We got a location for the face sensor, but we unlocked with fingerprint.
-            val faceLocation = Point(250, 0)
-            fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
+        // We got a source but still have no sensor locations, so should be sticking with
+        // the default effect.
+        fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
 
-            runCurrent()
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
-            )
+        runCurrent()
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+        )
 
-            // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
-            val fingerprintLocation = Point(500, 500)
-            fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
-            fakeKeyguardRepository.setBiometricUnlockSource(
-                BiometricUnlockSource.FINGERPRINT_SENSOR
-            )
-            fakeKeyguardRepository.setBiometricUnlockState(
-                BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
-            )
+        // We got a location for the face sensor, but we unlocked with fingerprint.
+        val faceLocation = Point(250, 0)
+        fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
 
-            // We should now have switched to the circle reveal, at the fingerprint location.
-            runCurrent()
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
-                {
-                    it is CircleReveal &&
-                        it.centerX == fingerprintLocation.x &&
-                        it.centerY == fingerprintLocation.y
-                },
-            )
+        runCurrent()
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+        )
 
-            // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
-            val valuesPrevSize = values.size
-            fakeKeyguardRepository.setBiometricUnlockState(
-                BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
-            )
-            fakeKeyguardRepository.setBiometricUnlockState(
-                BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
-            )
-            assertEquals(valuesPrevSize, values.size)
+        // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
+        val fingerprintLocation = Point(500, 500)
+        fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
+        fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
 
-            // Non-biometric unlock, we should return to the default reveal.
-            fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+        // We should now have switched to the circle reveal, at the fingerprint location.
+        runCurrent()
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+            {
+                it is CircleReveal &&
+                    it.centerX == fingerprintLocation.x &&
+                    it.centerY == fingerprintLocation.y
+            },
+        )
 
-            runCurrent()
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
-                {
-                    it is CircleReveal &&
-                        it.centerX == fingerprintLocation.x &&
-                        it.centerY == fingerprintLocation.y
-                },
-                { it == DEFAULT_REVEAL_EFFECT },
-            )
+        // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
+        val valuesPrevSize = values.size
+        fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
+        fakeKeyguardRepository.setBiometricUnlockState(
+            BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+        )
+        assertEquals(valuesPrevSize, values.size)
 
-            // We already have a face location, so switching to face source should update the
-            // CircleReveal.
-            fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
-            runCurrent()
-            fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
-            runCurrent()
+        // Non-biometric unlock, we should return to the default reveal.
+        fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
 
-            values.assertEffectsMatchPredicates(
-                { it == DEFAULT_REVEAL_EFFECT },
-                {
-                    it is CircleReveal &&
-                        it.centerX == fingerprintLocation.x &&
-                        it.centerY == fingerprintLocation.y
-                },
-                { it == DEFAULT_REVEAL_EFFECT },
-                {
-                    it is CircleReveal &&
-                        it.centerX == faceLocation.x &&
-                        it.centerY == faceLocation.y
-                },
-            )
+        runCurrent()
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+            {
+                it is CircleReveal &&
+                    it.centerX == fingerprintLocation.x &&
+                    it.centerY == fingerprintLocation.y
+            },
+            { it == DEFAULT_REVEAL_EFFECT },
+        )
 
-            job.cancel()
+        // We already have a face location, so switching to face source should update the
+        // CircleReveal.
+        fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
+        runCurrent()
+        fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+        runCurrent()
+
+        values.assertEffectsMatchPredicates(
+            { it == DEFAULT_REVEAL_EFFECT },
+            {
+                it is CircleReveal &&
+                    it.centerX == fingerprintLocation.x &&
+                    it.centerY == fingerprintLocation.y
+            },
+            { it == DEFAULT_REVEAL_EFFECT },
+            { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y },
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    fun revealAmount_emitsTo1AfterAnimationStarted() =
+        runTest(UnconfinedTestDispatcher()) {
+            val value by collectLastValue(underTest.revealAmount)
+            underTest.startRevealAmountAnimator(true)
+            assertEquals(0.0f, value)
+            animatorTestRule.advanceTimeBy(500L)
+            assertEquals(1.0f, value)
+        }
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
+        runTest(UnconfinedTestDispatcher()) {
+            val value by collectLastValue(underTest.revealAmount)
+            underTest.startRevealAmountAnimator(false)
+            assertEquals(1.0f, value)
+            animatorTestRule.advanceTimeBy(500L)
+            assertEquals(0.0f, value)
         }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index ced0a21..8c9ed5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
@@ -73,6 +74,8 @@
     private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
     private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository
+    private lateinit var fakeDeviceEntryFingerprintAuthRepository:
+        FakeDeviceEntryFingerprintAuthRepository
 
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
@@ -94,6 +97,7 @@
                 )
                 .keyguardTransitionInteractor
 
+        fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
         underTest =
             SystemUIKeyguardFaceAuthInteractor(
                 mContext,
@@ -127,6 +131,7 @@
                 featureFlags,
                 FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
                 keyguardUpdateMonitor,
+                fakeDeviceEntryFingerprintAuthRepository
             )
     }
 
@@ -335,4 +340,14 @@
             assertThat(faceAuthRepository.runningAuthRequest.value)
                 .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false))
         }
+
+    @Test
+    fun faceUnlockIsDisabledWhenFpIsLockedOut() = testScope.runTest {
+        underTest.start()
+
+        fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+        runCurrent()
+
+        assertThat(faceAuthRepository.wasDisabled).isTrue()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 6e7ba6d..906d948 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -27,27 +27,37 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.statusbar.LightRevealScrim
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.Spy
 
 @SmallTest
 @RoboPilotTest
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class LightRevealScrimInteractorTest : SysuiTestCase() {
     private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
-    private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+    @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+    private val testScope = TestScope()
 
     private val keyguardTransitionInteractor =
         KeyguardTransitionInteractorFactory.create(
-                scope = TestScope().backgroundScope,
+                scope = testScope.backgroundScope,
                 repository = fakeKeyguardTransitionRepository,
             )
             .keyguardTransitionInteractor
@@ -69,9 +79,9 @@
         MockitoAnnotations.initMocks(this)
         underTest =
             LightRevealScrimInteractor(
-                fakeKeyguardTransitionRepository,
                 keyguardTransitionInteractor,
-                fakeLightRevealScrimRepository
+                fakeLightRevealScrimRepository,
+                testScope.backgroundScope
             )
     }
 
@@ -110,52 +120,36 @@
         }
 
     @Test
-    fun revealAmount_invertedWhenAppropriate() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-            val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
+    fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() =
+        testScope.runTest {
             fakeKeyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 0.3f
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.OFF,
+                    to = KeyguardState.OFF
                 )
             )
-
-            assertEquals(values, listOf(0.3f))
+            runCurrent()
+            verify(fakeLightRevealScrimRepository, never()).startRevealAmountAnimator(anyBoolean())
 
             fakeKeyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN
+                )
+            )
+            runCurrent()
+            verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true)
+
+            fakeKeyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
                     from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 0.3f
+                    to = KeyguardState.DOZING
                 )
             )
-
-            assertEquals(values, listOf(0.3f, 0.7f))
-
-            job.cancel()
-        }
-
-    @Test
-    fun revealAmount_ignoresTransitionsThatDoNotAffectRevealAmount() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-            val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
-            fakeKeyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f)
-            )
-
-            assertEquals(values, emptyList<Float>())
-
-            fakeKeyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f)
-            )
-
-            assertEquals(values, emptyList<Float>())
-
-            job.cancel()
+            runCurrent()
+            verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false)
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
new file mode 100644
index 0000000..f5a70f0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.model
+
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SysUiStateExtTest : SysuiTestCase() {
+
+    private val underTest = SysUiState(FakeDisplayTracker(context))
+
+    @Test
+    fun updateFlags() {
+        underTest.updateFlags(
+            Display.DEFAULT_DISPLAY,
+            1 to true,
+            2 to false,
+            3 to true,
+        )
+
+        assertThat(underTest.flags and 1).isNotEqualTo(0)
+        assertThat(underTest.flags and 2).isEqualTo(0)
+        assertThat(underTest.flags and 3).isNotEqualTo(0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
deleted file mode 100644
index ceacaf9..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.repository
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeRepositoryTest : SysuiTestCase() {
-
-    private lateinit var inputProxy: MultiShadeInputProxy
-
-    @Before
-    fun setUp() {
-        inputProxy = MultiShadeInputProxy()
-    }
-
-    @Test
-    fun proxiedInput() = runTest {
-        val underTest = create()
-        val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
-
-        assertWithMessage("proxiedInput should start with null").that(latest).isNull()
-
-        inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
-        assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
-
-        inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
-        assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
-
-        inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
-        assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
-
-        inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
-        assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
-    }
-
-    @Test
-    fun shadeConfig_dualShadeEnabled() = runTest {
-        overrideResource(R.bool.dual_shade_enabled, true)
-        val underTest = create()
-        val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
-        assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
-    }
-
-    @Test
-    fun shadeConfig_dualShadeNotEnabled() = runTest {
-        overrideResource(R.bool.dual_shade_enabled, false)
-        val underTest = create()
-        val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
-        assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
-    }
-
-    @Test
-    fun forceCollapseAll() = runTest {
-        val underTest = create()
-        val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
-
-        assertWithMessage("forceCollapseAll should start as false!")
-            .that(forceCollapseAll)
-            .isFalse()
-
-        underTest.setForceCollapseAll(true)
-        assertThat(forceCollapseAll).isTrue()
-
-        underTest.setForceCollapseAll(false)
-        assertThat(forceCollapseAll).isFalse()
-    }
-
-    @Test
-    fun shadeInteraction() = runTest {
-        val underTest = create()
-        val shadeInteraction: MultiShadeInteractionModel? by
-            collectLastValue(underTest.shadeInteraction)
-
-        assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
-
-        underTest.setShadeInteraction(
-            MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
-        )
-        assertThat(shadeInteraction)
-            .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
-
-        underTest.setShadeInteraction(
-            MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
-        )
-        assertThat(shadeInteraction)
-            .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
-
-        underTest.setShadeInteraction(null)
-        assertThat(shadeInteraction).isNull()
-    }
-
-    @Test
-    fun expansion() = runTest {
-        val underTest = create()
-        val leftExpansion: Float? by
-            collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
-        val rightExpansion: Float? by
-            collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
-        val singleExpansion: Float? by
-            collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
-
-        assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
-        assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
-        assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
-
-        underTest.setExpansion(
-            shadeId = ShadeId.LEFT,
-            0.4f,
-        )
-        assertThat(leftExpansion).isEqualTo(0.4f)
-        assertThat(rightExpansion).isEqualTo(0f)
-        assertThat(singleExpansion).isEqualTo(0f)
-
-        underTest.setExpansion(
-            shadeId = ShadeId.RIGHT,
-            0.73f,
-        )
-        assertThat(leftExpansion).isEqualTo(0.4f)
-        assertThat(rightExpansion).isEqualTo(0.73f)
-        assertThat(singleExpansion).isEqualTo(0f)
-
-        underTest.setExpansion(
-            shadeId = ShadeId.LEFT,
-            0.1f,
-        )
-        underTest.setExpansion(
-            shadeId = ShadeId.SINGLE,
-            0.88f,
-        )
-        assertThat(leftExpansion).isEqualTo(0.1f)
-        assertThat(rightExpansion).isEqualTo(0.73f)
-        assertThat(singleExpansion).isEqualTo(0.88f)
-    }
-
-    private fun create(): MultiShadeRepository {
-        return create(
-            context = context,
-            inputProxy = inputProxy,
-        )
-    }
-
-    companion object {
-        fun create(
-            context: Context,
-            inputProxy: MultiShadeInputProxy,
-        ): MultiShadeRepository {
-            return MultiShadeRepository(
-                applicationContext = context,
-                inputProxy = inputProxy,
-            )
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
deleted file mode 100644
index bcc99bc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeInteractorTest : SysuiTestCase() {
-
-    private lateinit var testScope: TestScope
-    private lateinit var inputProxy: MultiShadeInputProxy
-
-    @Before
-    fun setUp() {
-        testScope = TestScope()
-        inputProxy = MultiShadeInputProxy()
-    }
-
-    @Test
-    fun maxShadeExpansion() =
-        testScope.runTest {
-            val underTest = create()
-            val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
-            assertWithMessage("maxShadeExpansion must start with 0.0!")
-                .that(maxShadeExpansion)
-                .isEqualTo(0f)
-
-            underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
-            assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
-            underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
-            assertThat(maxShadeExpansion).isEqualTo(0.442f)
-
-            underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
-            assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
-            underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
-            assertThat(maxShadeExpansion).isEqualTo(0f)
-        }
-
-    @Test
-    fun isAnyShadeExpanded() =
-        testScope.runTest {
-            val underTest = create()
-            val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded)
-            assertWithMessage("isAnyShadeExpanded must start with false!")
-                .that(isAnyShadeExpanded)
-                .isFalse()
-
-            underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
-            assertThat(isAnyShadeExpanded).isTrue()
-
-            underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
-            assertThat(isAnyShadeExpanded).isTrue()
-
-            underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
-            assertThat(isAnyShadeExpanded).isTrue()
-
-            underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
-            assertThat(isAnyShadeExpanded).isFalse()
-        }
-
-    @Test
-    fun isVisible_dualShadeConfig() =
-        testScope.runTest {
-            overrideResource(R.bool.dual_shade_enabled, true)
-            val underTest = create()
-            val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
-            val isRightShadeVisible: Boolean? by
-                collectLastValue(underTest.isVisible(ShadeId.RIGHT))
-            val isSingleShadeVisible: Boolean? by
-                collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
-            assertThat(isLeftShadeVisible).isTrue()
-            assertThat(isRightShadeVisible).isTrue()
-            assertThat(isSingleShadeVisible).isFalse()
-        }
-
-    @Test
-    fun isVisible_singleShadeConfig() =
-        testScope.runTest {
-            overrideResource(R.bool.dual_shade_enabled, false)
-            val underTest = create()
-            val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
-            val isRightShadeVisible: Boolean? by
-                collectLastValue(underTest.isVisible(ShadeId.RIGHT))
-            val isSingleShadeVisible: Boolean? by
-                collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
-            assertThat(isLeftShadeVisible).isFalse()
-            assertThat(isRightShadeVisible).isFalse()
-            assertThat(isSingleShadeVisible).isTrue()
-        }
-
-    @Test
-    fun isNonProxiedInputAllowed() =
-        testScope.runTest {
-            val underTest = create()
-            val isLeftShadeNonProxiedInputAllowed: Boolean? by
-                collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
-            assertWithMessage("isNonProxiedInputAllowed should start as true!")
-                .that(isLeftShadeNonProxiedInputAllowed)
-                .isTrue()
-
-            // Need to collect proxied input so the flows become hot as the gesture cancelation code
-            // logic sits in side the proxiedInput flow for each shade.
-            collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
-            collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
-
-            // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
-            // the
-            // same shade.
-            inputProxy.onProxiedInput(
-                ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
-            )
-            assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
-            // Registering the end of the proxied interaction re-allows it.
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
-            assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
-
-            // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
-            // disallowing non-proxied input on the LEFT shade.
-            inputProxy.onProxiedInput(
-                ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
-            )
-            assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
-            // Registering the end of the interaction on the RIGHT shade re-allows it.
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
-            assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
-        }
-
-    @Test
-    fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
-        testScope.runTest {
-            val underTest = create()
-            val isLeftShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
-            val isRightShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
-            val isSingleShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isLeftShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isRightShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isSingleShadeForceCollapsed)
-                .isFalse()
-
-            // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
-            // shade.
-            underTest.onUserInteractionStarted(ShadeId.RIGHT)
-            assertThat(isLeftShadeForceCollapsed).isTrue()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the end of the interaction on the RIGHT shade re-allows it.
-            underTest.onUserInteractionEnded(ShadeId.RIGHT)
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
-            // shade.
-            underTest.onUserInteractionStarted(ShadeId.LEFT)
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the end of the interaction on the LEFT shade re-allows it.
-            underTest.onUserInteractionEnded(ShadeId.LEFT)
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-        }
-
-    @Test
-    fun collapseAll() =
-        testScope.runTest {
-            val underTest = create()
-            val isLeftShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
-            val isRightShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
-            val isSingleShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isLeftShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isRightShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isSingleShadeForceCollapsed)
-                .isFalse()
-
-            underTest.collapseAll()
-            assertThat(isLeftShadeForceCollapsed).isTrue()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isTrue()
-
-            // Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
-            // "collapse all". Note that now the RIGHT shade is force-collapsed because we're
-            // interacting with the LEFT shade.
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-        }
-
-    @Test
-    fun onTapOutside_collapsesAll() =
-        testScope.runTest {
-            val underTest = create()
-            val isLeftShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
-            val isRightShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
-            val isSingleShadeForceCollapsed: Boolean? by
-                collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isLeftShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isRightShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isSingleShadeForceCollapsed)
-                .isFalse()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
-            assertThat(isLeftShadeForceCollapsed).isTrue()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isTrue()
-        }
-
-    @Test
-    fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
-        testScope.runTest {
-            val underTest = create()
-            val proxiedInput: ProxiedInputModel? by
-                collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
-            underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
-            assertThat(proxiedInput).isNull()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
-            assertThat(proxiedInput).isNull()
-
-            underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
-            assertThat(proxiedInput).isNotNull()
-        }
-
-    private fun create(): MultiShadeInteractor {
-        return create(
-            testScope = testScope,
-            context = context,
-            inputProxy = inputProxy,
-        )
-    }
-
-    companion object {
-        fun create(
-            testScope: TestScope,
-            context: Context,
-            inputProxy: MultiShadeInputProxy,
-        ): MultiShadeInteractor {
-            return MultiShadeInteractor(
-                applicationScope = testScope.backgroundScope,
-                repository =
-                    MultiShadeRepositoryTest.create(
-                        context = context,
-                        inputProxy = inputProxy,
-                    ),
-                inputProxy = inputProxy,
-            )
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
deleted file mode 100644
index 5890cbd..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
+++ /dev/null
@@ -1,530 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.shade.ShadeController
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.currentTime
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
-
-    private lateinit var underTest: MultiShadeMotionEventInteractor
-
-    private lateinit var testScope: TestScope
-    private lateinit var motionEvents: MutableSet<MotionEvent>
-    private lateinit var repository: MultiShadeRepository
-    private lateinit var interactor: MultiShadeInteractor
-    private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
-    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var falsingManager: FalsingManagerFake
-    @Mock private lateinit var shadeController: ShadeController
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
-        motionEvents = mutableSetOf()
-
-        val inputProxy = MultiShadeInputProxy()
-        repository =
-            MultiShadeRepository(
-                applicationContext = context,
-                inputProxy = inputProxy,
-            )
-        interactor =
-            MultiShadeInteractor(
-                applicationScope = testScope.backgroundScope,
-                repository = repository,
-                inputProxy = inputProxy,
-            )
-        val featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.DUAL_SHADE, true)
-        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        falsingManager = FalsingManagerFake()
-
-        underTest =
-            MultiShadeMotionEventInteractor(
-                applicationContext = context,
-                applicationScope = testScope.backgroundScope,
-                multiShadeInteractor = interactor,
-                featureFlags = featureFlags,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractorFactory.create(
-                            scope = TestScope().backgroundScope,
-                            repository = keyguardTransitionRepository,
-                        )
-                        .keyguardTransitionInteractor,
-                falsingManager = falsingManager,
-                shadeController = shadeController,
-            )
-    }
-
-    @After
-    fun tearDown() {
-        motionEvents.forEach { motionEvent -> motionEvent.recycle() }
-    }
-
-    @Test
-    fun listenForIsAnyShadeExpanded_expanded_makesWindowViewVisible() =
-        testScope.runTest {
-            whenever(shadeController.isKeyguard).thenReturn(false)
-            repository.setExpansion(ShadeId.LEFT, 0.1f)
-            val expanded by collectLastValue(interactor.isAnyShadeExpanded)
-            assertThat(expanded).isTrue()
-
-            verify(shadeController).makeExpandedVisible(anyBoolean())
-        }
-
-    @Test
-    fun listenForIsAnyShadeExpanded_collapsed_makesWindowViewInvisible() =
-        testScope.runTest {
-            whenever(shadeController.isKeyguard).thenReturn(false)
-            repository.setForceCollapseAll(true)
-            val expanded by collectLastValue(interactor.isAnyShadeExpanded)
-            assertThat(expanded).isFalse()
-
-            verify(shadeController).makeExpandedInvisible()
-        }
-
-    @Test
-    fun listenForIsAnyShadeExpanded_collapsedOnKeyguard_makesWindowViewVisible() =
-        testScope.runTest {
-            whenever(shadeController.isKeyguard).thenReturn(true)
-            repository.setForceCollapseAll(true)
-            val expanded by collectLastValue(interactor.isAnyShadeExpanded)
-            assertThat(expanded).isFalse()
-
-            verify(shadeController).makeExpandedVisible(anyBoolean())
-        }
-
-    @Test
-    fun shouldIntercept_initialDown_returnsFalse() =
-        testScope.runTest {
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_moveBelowTouchSlop_returnsFalse() =
-        testScope.runTest {
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop - 1f,
-                        )
-                    )
-                )
-                .isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlop_returnsTrue() =
-        testScope.runTest {
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isTrue()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() =
-        testScope.runTest {
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            x = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() =
-        testScope.runTest {
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue()
-        }
-
-    @Test
-    fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() =
-        testScope.runTest {
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() =
-        testScope.runTest {
-            repository.setExpansion(ShadeId.LEFT, 0.1f)
-            runCurrent()
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() =
-        testScope.runTest {
-            repository.setExpansion(ShadeId.LEFT, 0.1f)
-            runCurrent()
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlopAndUp_butBouncerShowing_returnsFalse() =
-        testScope.runTest {
-            keyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 0.1f,
-                    transitionState = TransitionState.STARTED,
-                )
-            )
-            runCurrent()
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
-        }
-
-    @Test
-    fun shouldIntercept_moveAboveTouchSlopAndCancel_butBouncerShowing_returnsFalse() =
-        testScope.runTest {
-            keyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 0.1f,
-                    transitionState = TransitionState.STARTED,
-                )
-            )
-            runCurrent()
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
-            assertThat(
-                    underTest.shouldIntercept(
-                        motionEvent(
-                            MotionEvent.ACTION_MOVE,
-                            y = touchSlop + 1f,
-                        )
-                    )
-                )
-                .isFalse()
-            assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
-        }
-
-    @Test
-    fun tap_doesNotSendProxiedInput() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    @Test
-    fun dragBelowTouchSlop_doesNotSendProxiedInput() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f))
-            underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    @Test
-    fun dragShadeAboveTouchSlopAndUp() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(
-                motionEvent(
-                    MotionEvent.ACTION_DOWN,
-                    x = 100f, // left shade
-                )
-            )
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val yDragAmountPx = touchSlop + 1f
-            val moveEvent =
-                motionEvent(
-                    MotionEvent.ACTION_MOVE,
-                    x = 100f, // left shade
-                    y = yDragAmountPx,
-                )
-            assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
-            underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput)
-                .isEqualTo(
-                    ProxiedInputModel.OnDrag(
-                        xFraction = 0.1f,
-                        yDragAmountPx = yDragAmountPx,
-                    )
-                )
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val upEvent = motionEvent(MotionEvent.ACTION_UP)
-            assertThat(underTest.shouldIntercept(upEvent)).isTrue()
-            underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    @Test
-    fun dragShadeAboveTouchSlopAndCancel() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(
-                motionEvent(
-                    MotionEvent.ACTION_DOWN,
-                    x = 900f, // right shade
-                )
-            )
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val yDragAmountPx = touchSlop + 1f
-            val moveEvent =
-                motionEvent(
-                    MotionEvent.ACTION_MOVE,
-                    x = 900f, // right shade
-                    y = yDragAmountPx,
-                )
-            assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
-            underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput)
-                .isEqualTo(
-                    ProxiedInputModel.OnDrag(
-                        xFraction = 0.9f,
-                        yDragAmountPx = yDragAmountPx,
-                    )
-                )
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
-            assertThat(underTest.shouldIntercept(cancelEvent)).isTrue()
-            underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    @Test
-    fun dragUp_withUp_doesNotShowShade() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(
-                motionEvent(
-                    MotionEvent.ACTION_DOWN,
-                    x = 100f, // left shade
-                )
-            )
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val yDragAmountPx = -(touchSlop + 1f) // dragging up
-            val moveEvent =
-                motionEvent(
-                    MotionEvent.ACTION_MOVE,
-                    x = 100f, // left shade
-                    y = yDragAmountPx,
-                )
-            assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
-            underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val upEvent = motionEvent(MotionEvent.ACTION_UP)
-            assertThat(underTest.shouldIntercept(upEvent)).isFalse()
-            underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    @Test
-    fun dragUp_withCancel_falseTouch_showsThenHidesBouncer() =
-        testScope.runTest {
-            val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
-            val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
-            val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
-            underTest.shouldIntercept(
-                motionEvent(
-                    MotionEvent.ACTION_DOWN,
-                    x = 900f, // right shade
-                )
-            )
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            val yDragAmountPx = -(touchSlop + 1f) // drag up
-            val moveEvent =
-                motionEvent(
-                    MotionEvent.ACTION_MOVE,
-                    x = 900f, // right shade
-                    y = yDragAmountPx,
-                )
-            assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
-            underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-
-            falsingManager.setIsFalseTouch(true)
-            val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
-            assertThat(underTest.shouldIntercept(cancelEvent)).isFalse()
-            underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
-            assertThat(leftShadeProxiedInput).isNull()
-            assertThat(rightShadeProxiedInput).isNull()
-            assertThat(singleShadeProxiedInput).isNull()
-        }
-
-    private fun TestScope.motionEvent(
-        action: Int,
-        downTime: Long = currentTime,
-        eventTime: Long = currentTime,
-        x: Float = 0f,
-        y: Float = 0f,
-    ): MotionEvent {
-        val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0)
-        motionEvents.add(motionEvent)
-        return motionEvent
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
deleted file mode 100644
index 8935309..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.shared.math
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SmallTest
-@RunWith(JUnit4::class)
-class MathTest : SysuiTestCase() {
-
-    @Test
-    fun isZero_zero_true() {
-        assertThat(0f.isZero(epsilon = EPSILON)).isTrue()
-    }
-
-    @Test
-    fun isZero_belowPositiveEpsilon_true() {
-        assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue()
-    }
-
-    @Test
-    fun isZero_aboveNegativeEpsilon_true() {
-        assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue()
-    }
-
-    @Test
-    fun isZero_positiveEpsilon_false() {
-        assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse()
-    }
-
-    @Test
-    fun isZero_negativeEpsilon_false() {
-        assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse()
-    }
-
-    @Test
-    fun isZero_positive_false() {
-        assertThat(1f.isZero(epsilon = EPSILON)).isFalse()
-    }
-
-    @Test
-    fun isZero_negative_false() {
-        assertThat((-1f).isZero(epsilon = EPSILON)).isFalse()
-    }
-
-    companion object {
-        private const val EPSILON = 0.0001f
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
deleted file mode 100644
index 0484515..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeViewModelTest : SysuiTestCase() {
-
-    private lateinit var testScope: TestScope
-    private lateinit var inputProxy: MultiShadeInputProxy
-
-    @Before
-    fun setUp() {
-        testScope = TestScope()
-        inputProxy = MultiShadeInputProxy()
-    }
-
-    @Test
-    fun scrim_whenDualShadeCollapsed() =
-        testScope.runTest {
-            val alpha = 0.5f
-            overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
-            overrideResource(R.bool.dual_shade_enabled, true)
-
-            val underTest = create()
-            val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
-            val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
-            assertThat(scrimAlpha).isZero()
-            assertThat(isScrimEnabled).isFalse()
-        }
-
-    @Test
-    fun scrim_whenDualShadeExpanded() =
-        testScope.runTest {
-            val alpha = 0.5f
-            overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
-            overrideResource(R.bool.dual_shade_enabled, true)
-            val underTest = create()
-            val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
-            val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-            assertThat(scrimAlpha).isZero()
-            assertThat(isScrimEnabled).isFalse()
-
-            underTest.leftShade.onExpansionChanged(0.5f)
-            assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
-            assertThat(isScrimEnabled).isTrue()
-
-            underTest.rightShade.onExpansionChanged(1f)
-            assertThat(scrimAlpha).isEqualTo(alpha * 1f)
-            assertThat(isScrimEnabled).isTrue()
-        }
-
-    @Test
-    fun scrim_whenSingleShadeCollapsed() =
-        testScope.runTest {
-            val alpha = 0.5f
-            overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
-            overrideResource(R.bool.dual_shade_enabled, false)
-
-            val underTest = create()
-            val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
-            val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
-            assertThat(scrimAlpha).isZero()
-            assertThat(isScrimEnabled).isFalse()
-        }
-
-    @Test
-    fun scrim_whenSingleShadeExpanded() =
-        testScope.runTest {
-            val alpha = 0.5f
-            overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
-            overrideResource(R.bool.dual_shade_enabled, false)
-            val underTest = create()
-            val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
-            val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
-            underTest.singleShade.onExpansionChanged(0.95f)
-
-            assertThat(scrimAlpha).isZero()
-            assertThat(isScrimEnabled).isFalse()
-        }
-
-    private fun create(): MultiShadeViewModel {
-        return MultiShadeViewModel(
-            viewModelScope = testScope.backgroundScope,
-            interactor =
-                MultiShadeInteractorTest.create(
-                    testScope = testScope,
-                    context = context,
-                    inputProxy = inputProxy,
-                ),
-        )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
deleted file mode 100644
index e32aac5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class ShadeViewModelTest : SysuiTestCase() {
-
-    private lateinit var testScope: TestScope
-    private lateinit var inputProxy: MultiShadeInputProxy
-    private var interactor: MultiShadeInteractor? = null
-
-    @Before
-    fun setUp() {
-        testScope = TestScope()
-        inputProxy = MultiShadeInputProxy()
-    }
-
-    @Test
-    fun isVisible_dualShadeConfig() =
-        testScope.runTest {
-            overrideResource(R.bool.dual_shade_enabled, true)
-            val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
-            val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
-            val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
-            assertThat(isLeftShadeVisible).isTrue()
-            assertThat(isRightShadeVisible).isTrue()
-            assertThat(isSingleShadeVisible).isFalse()
-        }
-
-    @Test
-    fun isVisible_singleShadeConfig() =
-        testScope.runTest {
-            overrideResource(R.bool.dual_shade_enabled, false)
-            val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
-            val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
-            val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
-            assertThat(isLeftShadeVisible).isFalse()
-            assertThat(isRightShadeVisible).isFalse()
-            assertThat(isSingleShadeVisible).isTrue()
-        }
-
-    @Test
-    fun isSwipingEnabled() =
-        testScope.runTest {
-            val underTest = create(ShadeId.LEFT)
-            val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
-            assertWithMessage("isSwipingEnabled should start as true!")
-                .that(isSwipingEnabled)
-                .isTrue()
-
-            // Need to collect proxied input so the flows become hot as the gesture cancelation code
-            // logic sits in side the proxiedInput flow for each shade.
-            collectLastValue(underTest.proxiedInput)
-            collectLastValue(create(ShadeId.RIGHT).proxiedInput)
-
-            // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
-            // the
-            // same shade.
-            inputProxy.onProxiedInput(
-                ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
-            )
-            assertThat(isSwipingEnabled).isFalse()
-
-            // Registering the end of the proxied interaction re-allows it.
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
-            assertThat(isSwipingEnabled).isTrue()
-
-            // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
-            // disallowing non-proxied input on the LEFT shade.
-            inputProxy.onProxiedInput(
-                ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
-            )
-            assertThat(isSwipingEnabled).isFalse()
-
-            // Registering the end of the interaction on the RIGHT shade re-allows it.
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
-            assertThat(isSwipingEnabled).isTrue()
-        }
-
-    @Test
-    fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
-        testScope.runTest {
-            val leftShade = create(ShadeId.LEFT)
-            val rightShade = create(ShadeId.RIGHT)
-            val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
-            val isRightShadeForceCollapsed: Boolean? by
-                collectLastValue(rightShade.isForceCollapsed)
-            val isSingleShadeForceCollapsed: Boolean? by
-                collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isLeftShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isRightShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isSingleShadeForceCollapsed)
-                .isFalse()
-
-            // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
-            // shade.
-            rightShade.onDragStarted()
-            assertThat(isLeftShadeForceCollapsed).isTrue()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the end of the interaction on the RIGHT shade re-allows it.
-            rightShade.onDragEnded()
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
-            // shade.
-            leftShade.onDragStarted()
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-
-            // Registering the end of the interaction on the LEFT shade re-allows it.
-            leftShade.onDragEnded()
-            assertThat(isLeftShadeForceCollapsed).isFalse()
-            assertThat(isRightShadeForceCollapsed).isFalse()
-            assertThat(isSingleShadeForceCollapsed).isFalse()
-        }
-
-    @Test
-    fun onTapOutside_collapsesAll() =
-        testScope.runTest {
-            val isLeftShadeForceCollapsed: Boolean? by
-                collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
-            val isRightShadeForceCollapsed: Boolean? by
-                collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
-            val isSingleShadeForceCollapsed: Boolean? by
-                collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isLeftShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isRightShadeForceCollapsed)
-                .isFalse()
-            assertWithMessage("isForceCollapsed should start as false!")
-                .that(isSingleShadeForceCollapsed)
-                .isFalse()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
-            assertThat(isLeftShadeForceCollapsed).isTrue()
-            assertThat(isRightShadeForceCollapsed).isTrue()
-            assertThat(isSingleShadeForceCollapsed).isTrue()
-        }
-
-    @Test
-    fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
-        testScope.runTest {
-            val underTest = create(ShadeId.RIGHT)
-            val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
-            underTest.onDragStarted()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
-            assertThat(proxiedInput).isNull()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
-            assertThat(proxiedInput).isNull()
-
-            underTest.onDragEnded()
-
-            inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
-            assertThat(proxiedInput).isNotNull()
-        }
-
-    private fun create(
-        shadeId: ShadeId,
-    ): ShadeViewModel {
-        return ShadeViewModel(
-            viewModelScope = testScope.backgroundScope,
-            shadeId = shadeId,
-            interactor = interactor
-                    ?: MultiShadeInteractorTest.create(
-                            testScope = testScope,
-                            context = context,
-                            inputProxy = inputProxy,
-                        )
-                        .also { interactor = it },
-        )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 25d494c..cbfad56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -94,6 +94,7 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.CommandQueue;
@@ -467,6 +468,7 @@
         when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
         return spy(new NavigationBar(
                 mNavigationBarView,
+                mock(ShadeController.class),
                 mNavigationBarFrame,
                 null,
                 context,
@@ -485,7 +487,7 @@
                 Optional.of(mock(Pip.class)),
                 Optional.of(mock(Recents.class)),
                 () -> Optional.of(mCentralSurfaces),
-                mock(ShadeController.class),
+                mock(ShadeViewController.class),
                 mock(NotificationRemoteInputManager.class),
                 mock(NotificationShadeDepthController.class),
                 mHandler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index 5b30687..3e20511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
@@ -31,6 +31,7 @@
 
 import android.animation.Animator;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.PixelFormat;
 import android.graphics.drawable.Drawable;
@@ -90,6 +91,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 @SmallTest
@@ -182,6 +184,8 @@
     private List<WifiEntry> mAccessPoints = new ArrayList<>();
     private List<WifiEntry> mWifiEntries = new ArrayList<>();
 
+    private Configuration mConfig;
+
     @Before
     public void setUp() {
         mStaticMockSession = mockitoSession()
@@ -226,11 +230,17 @@
         mInternetDialogController.mActivityStarter = mActivityStarter;
         mInternetDialogController.mWifiIconInjector = mWifiIconInjector;
         mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, false);
+
+        mConfig = new Configuration(mContext.getResources().getConfiguration());
+        Configuration c2 = new Configuration(mConfig);
+        c2.setLocale(Locale.US);
+        mContext.getResources().updateConfiguration(c2, null);
     }
 
     @After
     public void tearDown() {
         mStaticMockSession.finishMocking();
+        mContext.getResources().updateConfiguration(mConfig, null);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 355c4b6..6bb13ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP
 import com.android.systemui.settings.FakeDisplayTracker
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shared.recents.IOverviewProxy
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK
 import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP
@@ -93,6 +94,7 @@
     @Mock private lateinit var shellInterface: ShellInterface
     @Mock private lateinit var navBarController: NavigationBarController
     @Mock private lateinit var centralSurfaces: CentralSurfaces
+    @Mock private lateinit var shadeViewController: ShadeViewController
     @Mock private lateinit var navModeController: NavigationModeController
     @Mock private lateinit var statusBarWinController: NotificationShadeWindowController
     @Mock private lateinit var userTracker: UserTracker
@@ -132,6 +134,7 @@
                 shellInterface,
                 Lazy { navBarController },
                 Lazy { Optional.of(centralSurfaces) },
+                Lazy { shadeViewController },
                 navModeController,
                 statusBarWinController,
                 sysUiState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index 5638d70..6f6c5a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -14,8 +14,11 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.scene.domain.startable
 
+import android.view.Display
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -23,17 +26,24 @@
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneContainerNames
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 
 @SmallTest
 @RunWith(JUnit4::class)
@@ -53,6 +63,7 @@
         utils.keyguardInteractor(
             repository = keyguardRepository,
         )
+    private val sysUiState: SysUiState = mock()
 
     private val underTest =
         SystemUiDefaultSceneContainerStartable(
@@ -61,6 +72,8 @@
             authenticationInteractor = authenticationInteractor,
             keyguardInteractor = keyguardInteractor,
             featureFlags = featureFlags,
+            sysUiState = sysUiState,
+            displayId = Display.DEFAULT_DISPLAY,
         )
 
     @Before
@@ -375,6 +388,31 @@
             assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
         }
 
+    @Test
+    fun hydrateSystemUiState() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+            clearInvocations(sysUiState)
+
+            listOf(
+                    SceneKey.Gone,
+                    SceneKey.Lockscreen,
+                    SceneKey.Bouncer,
+                    SceneKey.Shade,
+                    SceneKey.QuickSettings,
+                )
+                .forEachIndexed { index, sceneKey ->
+                    sceneInteractor.setCurrentScene(
+                        SceneContainerNames.SYSTEM_UI_DEFAULT,
+                        SceneModel(sceneKey),
+                    )
+                    runCurrent()
+
+                    verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+                }
+        }
+
     private fun prepareState(
         isFeatureEnabled: Boolean = true,
         isDeviceUnlocked: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index ecd3308..202c7bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -108,7 +108,6 @@
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.ActivityStarter;
@@ -299,7 +298,6 @@
     @Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
 
     @Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
-    @Mock protected MultiShadeInteractor mMultiShadeInteractor;
     @Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
     @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
     @Mock protected MotionEvent mDownMotionEvent;
@@ -615,7 +613,6 @@
                 mLockscreenToOccludedTransitionViewModel,
                 mMainDispatcher,
                 mKeyguardTransitionInteractor,
-                () -> mMultiShadeInteractor,
                 mDumpManager,
                 mKeyuardLongPressViewModel,
                 mKeyguardInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 2a398c55..893123d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -34,21 +34,15 @@
 import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -67,6 +61,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
@@ -80,9 +75,8 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -117,8 +111,9 @@
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
     @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
-    @Mock private lateinit var unfoldTransitionProgressProvider:
-            Optional<UnfoldTransitionProgressProvider>
+    @Mock
+    private lateinit var unfoldTransitionProgressProvider:
+        Optional<UnfoldTransitionProgressProvider>
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock
     lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -146,23 +141,11 @@
         val featureFlags = FakeFeatureFlags()
         featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
         featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
-        featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
         featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
 
-        val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
-        val multiShadeInteractor =
-            MultiShadeInteractor(
-                applicationScope = testScope.backgroundScope,
-                repository =
-                    MultiShadeRepository(
-                        applicationContext = context,
-                        inputProxy = inputProxy,
-                    ),
-                inputProxy = inputProxy,
-            )
         underTest =
             NotificationShadeWindowViewController(
                 lockscreenShadeTransitionController,
@@ -193,25 +176,14 @@
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
                 featureFlags,
-                { multiShadeInteractor },
                 FakeSystemClock(),
-                {
-                    MultiShadeMotionEventInteractor(
-                        applicationContext = context,
-                        applicationScope = testScope.backgroundScope,
-                        multiShadeInteractor = multiShadeInteractor,
-                        featureFlags = featureFlags,
-                        keyguardTransitionInteractor =
-                            KeyguardTransitionInteractorFactory.create(
-                                    scope = TestScope().backgroundScope,
-                            ).keyguardTransitionInteractor,
-                        falsingManager = FalsingManagerFake(),
-                        shadeController = shadeController,
-                    )
-                },
-                BouncerMessageInteractor(FakeBouncerMessageRepository(),
-                        mock(BouncerMessageFactory::class.java),
-                        FakeUserRepository(), CountDownTimerUtil(), featureFlags),
+                BouncerMessageInteractor(
+                    FakeBouncerMessageRepository(),
+                    mock(BouncerMessageFactory::class.java),
+                    FakeUserRepository(),
+                    CountDownTimerUtil(),
+                    featureFlags
+                ),
                 BouncerLogger(logcatLogBuffer("BouncerLog"))
             )
         underTest.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index d9eb9b9..ed4ac35c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -34,20 +34,14 @@
 import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -70,7 +64,6 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
@@ -85,7 +78,6 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -160,22 +152,10 @@
         val featureFlags = FakeFeatureFlags()
         featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
         featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
-        featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
         featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
-        val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
-        val multiShadeInteractor =
-            MultiShadeInteractor(
-                applicationScope = testScope.backgroundScope,
-                repository =
-                    MultiShadeRepository(
-                        applicationContext = context,
-                        inputProxy = inputProxy,
-                    ),
-                inputProxy = inputProxy,
-            )
         controller =
             NotificationShadeWindowViewController(
                 lockscreenShadeTransitionController,
@@ -206,23 +186,7 @@
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
                 featureFlags,
-                { multiShadeInteractor },
                 FakeSystemClock(),
-                {
-                    MultiShadeMotionEventInteractor(
-                        applicationContext = context,
-                        applicationScope = testScope.backgroundScope,
-                        multiShadeInteractor = multiShadeInteractor,
-                        featureFlags = featureFlags,
-                        keyguardTransitionInteractor =
-                            KeyguardTransitionInteractorFactory.create(
-                                    scope = TestScope().backgroundScope,
-                                )
-                                .keyguardTransitionInteractor,
-                        falsingManager = FalsingManagerFake(),
-                        shadeController = shadeController,
-                    )
-                },
                 BouncerMessageInteractor(
                     FakeBouncerMessageRepository(),
                     Mockito.mock(BouncerMessageFactory::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 77a22ac..29bc64e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.FakePowerRepository
@@ -73,6 +74,8 @@
     @Mock
     private lateinit var userTracker: UserTracker
     @Mock
+    private lateinit var dozeInteractor: DozeInteractor
+    @Mock
     private lateinit var screenOffAnimationController: ScreenOffAnimationController
 
     private lateinit var powerRepository: FakePowerRepository
@@ -98,6 +101,7 @@
                 ambientDisplayConfiguration,
                 statusBarStateController,
                 shadeLogger,
+                dozeInteractor,
                 userTracker,
                 tunerService,
                 dumpManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
index ad908e7..aab4bc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
@@ -6,6 +6,8 @@
 import android.os.VibrationEffect
 import android.os.Vibrator
 import android.testing.AndroidTestingRunner
+import android.view.HapticFeedbackConstants
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.eq
@@ -33,6 +35,7 @@
 
     @Mock lateinit var vibrator: Vibrator
     @Mock lateinit var executor: Executor
+    @Mock lateinit var view: View
     @Captor lateinit var backgroundTaskCaptor: ArgumentCaptor<Runnable>
     lateinit var vibratorHelper: VibratorHelper
 
@@ -72,6 +75,21 @@
     }
 
     @Test
+    fun testPerformHapticFeedback() {
+        val constant = HapticFeedbackConstants.CONFIRM
+        vibratorHelper.performHapticFeedback(view, constant)
+        verify(view).performHapticFeedback(eq(constant))
+    }
+
+    @Test
+    fun testPerformHapticFeedback_withFlags() {
+        val constant = HapticFeedbackConstants.CONFIRM
+        val flag = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+        vibratorHelper.performHapticFeedback(view, constant, flag)
+        verify(view).performHapticFeedback(eq(constant), eq(flag))
+    }
+
+    @Test
     fun testHasVibrator() {
         assertThat(vibratorHelper.hasVibrator()).isTrue()
         verify(vibrator).hasVibrator()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java
index 6d687a6..cf1d2ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java
@@ -41,6 +41,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -60,12 +61,15 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Locale;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @UiThreadTest
@@ -87,6 +91,8 @@
     @Mock
     private NotificationGutsManager mNotificationGutsManager;
 
+    private Configuration mConfig;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -111,7 +117,15 @@
 
         mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
                 new Notification(), UserHandle.CURRENT, null, 0);
+        mConfig = new Configuration(mContext.getResources().getConfiguration());
+        Configuration c2 = new Configuration(mConfig);
+        c2.setLocale(Locale.US);
+        mContext.getResources().updateConfiguration(c2, null);
+    }
 
+    @After
+    public void tearDown() {
+        mContext.getResources().updateConfiguration(mConfig, null);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 5e0e140..68f2728 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -65,6 +66,7 @@
     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
     @Mock private lateinit var keyguardViewMediator: KeyguardViewMediator
     @Mock private lateinit var shadeController: ShadeController
+    @Mock private lateinit var shadeViewController: ShadeViewController
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
     @Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager
@@ -91,6 +93,7 @@
                 Lazy { biometricUnlockController },
                 Lazy { keyguardViewMediator },
                 Lazy { shadeController },
+                Lazy { shadeViewController },
                 Lazy { statusBarKeyguardViewManager },
                 Lazy { notifShadeWindowController },
                 activityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 479803e..4f8de3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -142,8 +142,7 @@
                 mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
                 mAuthController, mStatusBarStateController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
-                mSystemClock,
-                mStatusBarKeyguardViewManager
+                mSystemClock
         );
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
@@ -465,69 +464,6 @@
     }
 
     @Test
-    public void onSideFingerprintSuccess_dreaming_unlockThenWake() {
-        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
-        when(mWakefulnessLifecycle.getLastWakeReason())
-                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
-        final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
-                ArgumentCaptor.forClass(Runnable.class);
-        givenDreamingLocked();
-        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
-
-        // Make sure the BiometricUnlockController has registered a callback for when the keyguard
-        // is gone
-        verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
-                afterKeyguardGoneRunnableCaptor.capture());
-        // Ensure that the power hasn't been told to wake up yet.
-        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
-        // Check that the keyguard has been told to unlock.
-        verify(mKeyguardViewMediator).onWakeAndUnlocking();
-
-        // Simulate the keyguard disappearing.
-        afterKeyguardGoneRunnableCaptor.getValue().run();
-        // Verify that the power manager has been told to wake up now.
-        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
-    }
-
-    @Test
-    public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() {
-        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
-        when(mWakefulnessLifecycle.getLastWakeReason())
-                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
-        final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
-                ArgumentCaptor.forClass(Runnable.class);
-        givenDreamingLocked();
-        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
-
-        // Make sure the BiometricUnlockController has registered a callback for when the keyguard
-        // is gone
-        verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
-                afterKeyguardGoneRunnableCaptor.capture());
-        // Ensure that the power hasn't been told to wake up yet.
-        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
-        // Check that the keyguard has been told to unlock.
-        verify(mKeyguardViewMediator).onWakeAndUnlocking();
-
-        when(mUpdateMonitor.isDreaming()).thenReturn(false);
-        when(mKeyguardStateController.isUnlocked()).thenReturn(false);
-
-        // Simulate the keyguard disappearing.
-        afterKeyguardGoneRunnableCaptor.getValue().run();
-
-        final ArgumentCaptor<Runnable> dismissKeyguardRunnableCaptor =
-                ArgumentCaptor.forClass(Runnable.class);
-        verify(mHandler).post(dismissKeyguardRunnableCaptor.capture());
-
-        // Verify that the power manager was not told to wake up.
-        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
-
-        dismissKeyguardRunnableCaptor.getValue().run();
-        // Verify that the keyguard controller is told to unlock.
-        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
-    }
-
-
-    @Test
     public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
         // GIVEN side fingerprint enrolled, last wake reason was power button
         when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -601,14 +537,25 @@
         verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
     }
 
-    private void givenDreamingLocked() {
-        when(mUpdateMonitor.isDreaming()).thenReturn(true);
-        when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
-    }
-
     private void givenFingerprintModeUnlockCollapsing() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
     }
+
+    private void givenDreamingLocked() {
+        when(mUpdateMonitor.isDreaming()).thenReturn(true);
+        when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+    }
+    @Test
+    public void onSideFingerprintSuccess_dreaming_unlockNoWake() {
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+        when(mWakefulnessLifecycle.getLastWakeReason())
+                .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+        givenDreamingLocked();
+        mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
+        verify(mKeyguardViewMediator).onWakeAndUnlocking();
+        // Ensure that the power hasn't been told to wake up yet.
+        verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index c8ec1bf..28193db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -140,8 +140,6 @@
     @Test
     fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() {
         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
-        `when`(centralSurfacesImpl.shadeViewController)
-                .thenReturn(shadeViewController)
         `when`(shadeViewController.isViewEnabled).thenReturn(false)
         val returnVal = view.onTouchEvent(
                 MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
@@ -152,8 +150,6 @@
     @Test
     fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
-        `when`(centralSurfacesImpl.shadeViewController)
-                .thenReturn(shadeViewController)
         `when`(shadeViewController.isViewEnabled).thenReturn(false)
         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
 
@@ -165,8 +161,6 @@
     @Test
     fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() {
         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
-        `when`(centralSurfacesImpl.shadeViewController)
-                .thenReturn(shadeViewController)
         `when`(shadeViewController.isViewEnabled).thenReturn(true)
         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
 
@@ -178,8 +172,6 @@
     @Test
     fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() {
         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
-        `when`(centralSurfacesImpl.shadeViewController)
-                .thenReturn(shadeViewController)
         `when`(shadeViewController.isFullyCollapsed).thenReturn(true)
         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 2e9a690..e76f26d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -97,9 +97,7 @@
                 powerManager,
                 handler = handler
         )
-        controller.initialize(centralSurfaces, lightRevealScrim)
-        `when`(centralSurfaces.shadeViewController).thenReturn(
-            shadeViewController)
+        controller.initialize(centralSurfaces, shadeViewController, lightRevealScrim)
 
         // Screen off does not run if the panel is expanded, so we should say it's collapsed to test
         // screen off.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 813597a..7f990a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -101,7 +101,6 @@
         whenever(viewGroup.viewTreeObserver).thenReturn(viewTreeObserver)
         whenever(wakefulnessLifecycle.lastSleepReason)
             .thenReturn(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD)
-        whenever(centralSurfaces.shadeViewController).thenReturn(shadeViewController)
         whenever(shadeFoldAnimator.startFoldToAodAnimation(any(), any(), any())).then {
             val onActionStarted = it.arguments[0] as Runnable
             onActionStarted.run()
@@ -124,7 +123,7 @@
                         latencyTracker,
                         { keyguardInteractor },
                     )
-                    .apply { initialize(centralSurfaces, lightRevealScrim) }
+                    .apply { initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
 
             verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 0c5e438..2362a52 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -20,12 +20,16 @@
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
 
+    private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isInitialized = _isInitialized.asStateFlow()
+
     private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
-    override val sensorId = _sensorId.asStateFlow()
+    override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
 
     private val _strength: MutableStateFlow<SensorStrength> =
         MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -33,11 +37,12 @@
 
     private val _sensorType: MutableStateFlow<FingerprintSensorType> =
         MutableStateFlow(FingerprintSensorType.UNKNOWN)
-    override val sensorType = _sensorType.asStateFlow()
+    override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
 
     private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
         MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
-    override val sensorLocations = _sensorLocations.asStateFlow()
+    override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+        _sensorLocations.asStateFlow()
 
     fun setProperties(
         sensorId: Int,
@@ -49,5 +54,6 @@
         _strength.value = strength
         _sensorType.value = sensorType
         _sensorLocations.value = sensorLocations
+        _isInitialized.value = true
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 548169e..f4c2db1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -27,6 +27,11 @@
 
 class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
 
+    private var _wasDisabled: Boolean = false
+
+    val wasDisabled: Boolean
+        get() = _wasDisabled
+
     override val isAuthenticated = MutableStateFlow(false)
     override val canRunFaceAuth = MutableStateFlow(false)
     private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null)
@@ -52,6 +57,9 @@
     override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
 
     override val isBypassEnabled = MutableStateFlow(false)
+    override fun lockoutFaceAuth() {
+        _wasDisabled = true
+    }
 
     override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {
         _runningAuthRequest.value = uiEvent to fallbackToDetection
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
index 7c22604..b24b95e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.statusbar.LightRevealEffect
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of [LightRevealScrimRepository] */
@@ -30,4 +31,15 @@
     fun setRevealEffect(effect: LightRevealEffect) {
         _revealEffect.tryEmit(effect)
     }
+
+    private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f)
+    override val revealAmount: Flow<Float> = _revealAmount
+
+    override fun startRevealAmountAnimator(reveal: Boolean) {
+        if (reveal) {
+            _revealAmount.value = 1.0f
+        } else {
+            _revealAmount.value = 0.0f
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 3e7d759..631b453 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -47,7 +47,6 @@
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.Signature;
 import android.net.MacAddress;
 import android.os.Binder;
 import android.os.Bundle;
@@ -55,17 +54,11 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
-import android.util.Log;
-import android.util.PackageUtils;
 import android.util.Slog;
 
 import com.android.internal.R;
-import com.android.internal.util.ArrayUtils;
 
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 /**
  * Class responsible for handling incoming {@link AssociationRequest}s.
@@ -449,31 +442,6 @@
     };
 
     private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
-        // Below we check if the requesting package is allowlisted (usually by the OEM) for creating
-        // CDM associations without user confirmation (prompt).
-        // For this we'll check to config arrays:
-        // - com.android.internal.R.array.config_companionDevicePackages
-        // and
-        // - com.android.internal.R.array.config_companionDeviceCerts.
-        // Both arrays are expected to contain similar number of entries.
-        // config_companionDevicePackages contains package names of the allowlisted packages.
-        // config_companionDeviceCerts contains SHA256 digests of the signatures of the
-        // corresponding packages.
-        // If a package may be signed with one of several certificates, its package name would
-        // appear multiple times in the config_companionDevicePackages, with different entries
-        // (one for each of the valid signing certificates) at the corresponding positions in
-        // config_companionDeviceCerts.
-        final String[] allowlistedPackages = mContext.getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
-        if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
-            if (DEBUG) {
-                Log.d(TAG, packageName + " is not allowlisted for creating associations "
-                        + "without user confirmation (prompt)");
-                Log.v(TAG, "Allowlisted packages=" + Arrays.toString(allowlistedPackages));
-            }
-            return false;
-        }
-
         // Throttle frequent associations
         final long now = System.currentTimeMillis();
         final List<AssociationInfo> associationForPackage =
@@ -493,40 +461,6 @@
             }
         }
 
-        final String[] allowlistedPackagesSignatureDigests = mContext.getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
-        final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
-        for (int i = 0; i < allowlistedPackages.length; i++) {
-            if (allowlistedPackages[i].equals(packageName)) {
-                final String digest = allowlistedPackagesSignatureDigests[i].replaceAll(":", "");
-                allowlistedSignatureDigestsForRequestingPackage.add(digest);
-            }
-        }
-
-        final Signature[] requestingPackageSignatures = mPackageManager.getPackage(packageName)
-                .getSigningDetails().getSignatures();
-        final String[] requestingPackageSignatureDigests =
-                PackageUtils.computeSignaturesSha256Digests(requestingPackageSignatures);
-
-        boolean requestingPackageSignatureAllowlisted = false;
-        for (String signatureDigest : requestingPackageSignatureDigests) {
-            if (allowlistedSignatureDigestsForRequestingPackage.contains(signatureDigest)) {
-                requestingPackageSignatureAllowlisted = true;
-                break;
-            }
-        }
-
-        if (!requestingPackageSignatureAllowlisted) {
-            Slog.w(TAG, "Certificate mismatch for allowlisted package " + packageName);
-            if (DEBUG) {
-                Log.d(TAG, "  > allowlisted signatures for " + packageName + ": ["
-                        + String.join(", ", allowlistedSignatureDigestsForRequestingPackage)
-                        + "]");
-                Log.d(TAG, "  > actual signatures for " + packageName + ": "
-                        + Arrays.toString(requestingPackageSignatureDigests));
-            }
-        }
-
-        return requestingPackageSignatureAllowlisted;
+        return PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName);
     }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 89e8a18..4d4328d 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -247,7 +247,8 @@
         mCompanionAppController = new CompanionApplicationController(
                 context, mAssociationStore, mDevicePresenceMonitor);
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
-        mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore,
+        mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
+                mPackageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
         // TODO(b/279663946): move context sync to a dedicated system service
         mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index 3ab4aa8..db40fc4 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.GET_CONFIGURATIONS;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 
+import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.CompanionDeviceManagerService.TAG;
 
 import android.Manifest;
@@ -35,20 +36,28 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
 import android.os.Binder;
+import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.util.ArrayUtils;
+
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Utility methods for working with {@link PackageInfo}-s.
  */
-final class PackageUtils {
+public final class PackageUtils {
     private static final Intent COMPANION_SERVICE_INTENT =
             new Intent(CompanionDeviceService.SERVICE_INTERFACE);
     private static final String PROPERTY_PRIMARY_TAG =
@@ -141,4 +150,69 @@
             return false;
         }
     }
+
+    /**
+     * Check if the package is allowlisted in the overlay config.
+     * For this we'll check to config arrays:
+     *   - com.android.internal.R.array.config_companionDevicePackages
+     * and
+     *   - com.android.internal.R.array.config_companionDeviceCerts.
+     * Both arrays are expected to contain similar number of entries.
+     * config_companionDevicePackages contains package names of the allowlisted packages.
+     * config_companionDeviceCerts contains SHA256 digests of the signatures of the
+     * corresponding packages.
+     * If a package is signed with one of several certificates, its package name would
+     * appear multiple times in the config_companionDevicePackages, with different entries
+     * (one for each of the valid signing certificates) at the corresponding positions in
+     * config_companionDeviceCerts.
+     */
+    public static boolean isPackageAllowlisted(Context context,
+            PackageManagerInternal packageManagerInternal, @NonNull String packageName) {
+        final String[] allowlistedPackages = context.getResources()
+                .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
+        if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
+            if (DEBUG) {
+                Log.d(TAG, packageName + " is not allowlisted.");
+            }
+            return false;
+        }
+
+        final String[] allowlistedPackagesSignatureDigests = context.getResources()
+                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
+        final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
+        for (int i = 0; i < allowlistedPackages.length; i++) {
+            if (allowlistedPackages[i].equals(packageName)) {
+                final String digest = allowlistedPackagesSignatureDigests[i].replaceAll(":", "");
+                allowlistedSignatureDigestsForRequestingPackage.add(digest);
+            }
+        }
+
+        final Signature[] requestingPackageSignatures = packageManagerInternal.getPackage(
+                        packageName)
+                .getSigningDetails().getSignatures();
+        final String[] requestingPackageSignatureDigests =
+                android.util.PackageUtils.computeSignaturesSha256Digests(
+                        requestingPackageSignatures);
+
+        boolean requestingPackageSignatureAllowlisted = false;
+        for (String signatureDigest : requestingPackageSignatureDigests) {
+            if (allowlistedSignatureDigestsForRequestingPackage.contains(signatureDigest)) {
+                requestingPackageSignatureAllowlisted = true;
+                break;
+            }
+        }
+
+        if (!requestingPackageSignatureAllowlisted) {
+            Slog.w(TAG, "Certificate mismatch for allowlisted package " + packageName);
+            if (DEBUG) {
+                Log.d(TAG, "  > allowlisted signatures for " + packageName + ": ["
+                        + String.join(", ", allowlistedSignatureDigestsForRequestingPackage)
+                        + "]");
+                Log.d(TAG, "  > actual signatures for " + packageName + ": "
+                        + Arrays.toString(requestingPackageSignatureDigests));
+            }
+        }
+
+        return requestingPackageSignatureAllowlisted;
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index 82387a1..7af4957 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -37,6 +37,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -50,6 +51,7 @@
 import com.android.internal.R;
 import com.android.server.companion.AssociationStore;
 import com.android.server.companion.CompanionDeviceManagerService;
+import com.android.server.companion.PackageUtils;
 import com.android.server.companion.PermissionsUtils;
 import com.android.server.companion.transport.CompanionTransportManager;
 
@@ -77,6 +79,7 @@
             "system_data_transfer_result_receiver";
 
     private final Context mContext;
+    private final PackageManagerInternal mPackageManager;
     private final AssociationStore mAssociationStore;
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     private final CompanionTransportManager mTransportManager;
@@ -85,10 +88,12 @@
     private final ComponentName mCompanionDeviceDataTransferActivity;
 
     public SystemDataTransferProcessor(CompanionDeviceManagerService service,
+            PackageManagerInternal packageManager,
             AssociationStore associationStore,
             SystemDataTransferRequestStore systemDataTransferRequestStore,
             CompanionTransportManager transportManager) {
         mContext = service.getContext();
+        mPackageManager = packageManager;
         mAssociationStore = associationStore;
         mSystemDataTransferRequestStore = systemDataTransferRequestStore;
         mTransportManager = transportManager;
@@ -132,6 +137,11 @@
      */
     public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
             @UserIdInt int userId, int associationId) {
+        if (PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName)) {
+            Slog.i(LOG_TAG, "User consent Intent should be skipped. Returning null.");
+            return null;
+        }
+
         final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
 
         Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
@@ -175,23 +185,29 @@
         final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
 
         // Check if the request has been consented by the user.
-        List<SystemDataTransferRequest> storedRequests =
-                mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
-                        associationId);
-        boolean hasConsented = false;
-        for (SystemDataTransferRequest storedRequest : storedRequests) {
-            if (storedRequest instanceof PermissionSyncRequest && storedRequest.isUserConsented()) {
-                hasConsented = true;
-                break;
+        if (PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName)) {
+            Slog.i(LOG_TAG, "Skip user consent check due to the same OEM package.");
+        } else {
+            List<SystemDataTransferRequest> storedRequests =
+                    mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
+                            associationId);
+            boolean hasConsented = false;
+            for (SystemDataTransferRequest storedRequest : storedRequests) {
+                if (storedRequest instanceof PermissionSyncRequest
+                        && storedRequest.isUserConsented()) {
+                    hasConsented = true;
+                    break;
+                }
             }
-        }
-        if (!hasConsented) {
-            String message = "User " + userId + " hasn't consented permission sync.";
-            Slog.e(LOG_TAG, message);
-            try {
-                callback.onError(message);
-            } catch (RemoteException ignored) { }
-            return;
+            if (!hasConsented) {
+                String message = "User " + userId + " hasn't consented permission sync.";
+                Slog.e(LOG_TAG, message);
+                try {
+                    callback.onError(message);
+                } catch (RemoteException ignored) {
+                }
+                return;
+            }
         }
 
         // Start permission sync
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
index 279981b..d862a54 100644
--- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -9,6 +9,30 @@
       ]
     },
     {
+      "name": "CtsVirtualDevicesAudioTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsVirtualDevicesSensorTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsVirtualDevicesAppLaunchTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       "name": "CtsVirtualDevicesTestCases",
       "options": [
         {
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 6fd6afe..754a7ed 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -159,9 +159,10 @@
 
     private int getAllowedUid() {
         final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
-        final int mainUserId = umInternal.getMainUserId();
+        int mainUserId = umInternal.getMainUserId();
         if (mainUserId < 0) {
-            return -1;
+            // If main user is not defined. Use the SYSTEM user instead.
+            mainUserId = UserHandle.USER_SYSTEM;
         }
         String allowedPackage = mContext.getResources()
                 .getString(R.string.config_persistentDataPackageName);
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index 1e3a5f4..7a95d77 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -17,7 +17,7 @@
             "name": "CtsWindowManagerDeviceTestCases",
             "options": [
                 {
-                    "include-filter": "android.server.wm.ToastWindowTest"
+                    "include-filter": "android.server.wm.window.ToastWindowTest"
                 }
             ],
             "file_patterns": ["NotificationManagerService\\.java"]
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 146215b..d398260 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -4932,7 +4932,6 @@
             if (accountType == null) throw new IllegalArgumentException("accountType is null");
             mAccounts = accounts;
             mStripAuthTokenFromResult = stripAuthTokenFromResult;
-            mResponse = response;
             mAccountType = accountType;
             mExpectActivityLaunch = expectActivityLaunch;
             mCreationTime = SystemClock.elapsedRealtime();
@@ -4946,8 +4945,8 @@
             if (response != null) {
                 try {
                     response.asBinder().linkToDeath(this, 0 /* flags */);
+                    mResponse = response;
                 } catch (RemoteException e) {
-                    mResponse = null;
                     binderDied();
                 }
             }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index d199006..dc83125 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8169,7 +8169,13 @@
                 mAm.getUidStateLocked(r.mRecentCallingUid),
                 mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid),
                 0,
-                0);
+                0,
+                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+                r.mAllowWIUInBindService,
+                r.mAllowWIUByBindings,
+                r.mAllowStartForegroundNoBinding,
+                r.mAllowStartInBindService,
+                r.mAllowStartByBindings);
 
         int event = 0;
         if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 26bcb98..132d6a9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9013,7 +9013,9 @@
         final boolean isSystemApp = process == null ||
                 (process.info.flags & (ApplicationInfo.FLAG_SYSTEM |
                                        ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0;
-        final String processName = process == null ? "unknown" : process.processName;
+        final String processName = process != null && process.getPid() == MY_PID
+                ? "system_server"
+                : (process == null ? "unknown" : process.processName);
         final DropBoxManager dbox = (DropBoxManager)
                 mContext.getSystemService(Context.DROPBOX_SERVICE);
 
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index 4f5b5e1..786e1cc 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -506,7 +506,13 @@
                 ActivityManager.PROCESS_STATE_UNKNOWN,
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 apiDurationBeforeFgsStart,
-                apiDurationAfterFgsEnd);
+                apiDurationAfterFgsEnd,
+                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+                r.mAllowWIUInBindService,
+                r.mAllowWIUByBindings,
+                r.mAllowStartForegroundNoBinding,
+                r.mAllowStartInBindService,
+                r.mAllowStartByBindings);
     }
 
     /**
@@ -557,7 +563,13 @@
                 ActivityManager.PROCESS_STATE_UNKNOWN,
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 0,
-                apiDurationAfterFgsEnd);
+                apiDurationAfterFgsEnd,
+                0,
+                0,
+                0,
+                0,
+                0,
+                0);
     }
 
     /**
diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING
index 72d3835..68062b5 100644
--- a/services/core/java/com/android/server/appop/TEST_MAPPING
+++ b/services/core/java/com/android/server/appop/TEST_MAPPING
@@ -1,7 +1,12 @@
 {
     "presubmit": [
         {
-            "name": "CtsAppOpsTestCases"
+            "name": "CtsAppOpsTestCases",
+            "options": [
+                {
+                  "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                }
+            ]
         },
         {
             "name": "CtsAppOps2TestCases"
@@ -26,6 +31,9 @@
             "name": "CtsPermissionTestCases",
             "options": [
                 {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                },
+                {
                     "include-filter": "android.permission.cts.BackgroundPermissionsTest"
                 },
                 {
@@ -55,5 +63,27 @@
                 }
             ]
         }        
+    ],
+    "postsubmit": [
+        {
+            "name": "CtsAppOpsTestCases"
+        },
+        {
+            "name": "CtsPermissionTestCases",
+            "options": [
+                {
+                    "include-filter": "android.permission.cts.BackgroundPermissionsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SplitPermissionTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.PermissionFlagsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SharedUidPermissionsTest"
+                }
+            ]
+        }
     ]
 }
diff --git a/services/core/java/com/android/server/display/TEST_MAPPING b/services/core/java/com/android/server/display/TEST_MAPPING
index 57c2e01..6f243e1 100644
--- a/services/core/java/com/android/server/display/TEST_MAPPING
+++ b/services/core/java/com/android/server/display/TEST_MAPPING
@@ -1,15 +1,7 @@
 {
     "presubmit": [
         {
-            "name": "FrameworksMockingServicesTests",
-            "options": [
-                {"include-filter": "com.android.server.display"},
-                {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-                {"exclude-annotation": "androidx.test.filters.FlakyTest"}
-            ]
-        },
-        {
-            "name": "FrameworksServicesTests",
+            "name": "DisplayServiceTests",
             "options": [
                 {"include-filter": "com.android.server.display"},
                 {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
@@ -17,5 +9,14 @@
                 {"exclude-annotation": "org.junit.Ignore"}
             ]
         }
+    ],
+    "postsubmit": [
+        {
+            "name": "DisplayServiceTests",
+            "options": [
+                {"include-filter": "com.android.server.display"},
+                {"exclude-annotation": "org.junit.Ignore"}
+            ]
+        }
     ]
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index a3bca9a..d4d1bae 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -19,10 +19,12 @@
 import android.hardware.display.BrightnessInfo;
 import android.os.PowerManager;
 import android.os.SystemClock;
-import android.util.TimeUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
 /**
  * Represents a particular brightness change event.
  */
@@ -34,6 +36,8 @@
     public static final int FLAG_IDLE_CURVE = 0x10;
     public static final int FLAG_LOW_POWER_MODE = 0x20;
 
+    private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+
     private BrightnessReason mReason = new BrightnessReason();
     private int mDisplayId;
     private String mPhysicalDisplayId;
@@ -169,7 +173,7 @@
      * @return A stringified BrightnessEvent
      */
     public String toString(boolean includeTime) {
-        return (includeTime ? TimeUtils.formatForLogging(mTime) + " - " : "")
+        return (includeTime ? FORMAT.format(new Date(mTime)) + " - " : "")
                 + "BrightnessEvent: "
                 + "disp=" + mDisplayId
                 + ", physDisp=" + mPhysicalDisplayId
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9be86e8..e84be03 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1020,6 +1020,7 @@
         }
 
         mAssistants.resetDefaultAssistantsIfNecessary();
+        mPreferencesHelper.syncChannelsBypassingDnd();
     }
 
     @VisibleForTesting
@@ -1859,6 +1860,7 @@
                     mConditionProviders.onUserSwitched(userId);
                     mListeners.onUserSwitched(userId);
                     mZenModeHelper.onUserSwitched(userId);
+                    mPreferencesHelper.syncChannelsBypassingDnd();
                 }
                 // assistant is the only thing that cares about managed profiles specifically
                 mAssistants.onUserSwitched(userId);
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 1818d25..1bb1092 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -202,7 +202,7 @@
     private SparseBooleanArray mLockScreenShowNotifications;
     private SparseBooleanArray mLockScreenPrivateNotifications;
     private boolean mIsMediaNotificationFilteringEnabled = DEFAULT_MEDIA_NOTIFICATION_FILTERING;
-    private boolean mAreChannelsBypassingDnd;
+    private boolean mCurrentUserHasChannelsBypassingDnd;
     private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
     private boolean mShowReviewPermissionsNotification;
 
@@ -230,7 +230,6 @@
         updateBadgingEnabled();
         updateBubblesEnabled();
         updateMediaNotificationFilteringEnabled();
-        syncChannelsBypassingDnd(Process.SYSTEM_UID, true);  // init comes from system
     }
 
     public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
@@ -893,7 +892,7 @@
             r.groups.put(group.getId(), group);
         }
         if (needsDndChange) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
     }
 
@@ -972,7 +971,7 @@
                         existing.setBypassDnd(bypassDnd);
                         needsPolicyFileChange = true;
 
-                        if (bypassDnd != mAreChannelsBypassingDnd
+                        if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
                                 || previousExistingImportance != existing.getImportance()) {
                             needsDndChange = true;
                         }
@@ -1031,7 +1030,7 @@
                 }
 
                 r.channels.put(channel.getId(), channel);
-                if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
+                if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
                     needsDndChange = true;
                 }
                 MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1041,7 +1040,7 @@
         }
 
         if (needsDndChange) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
 
         return needsPolicyFileChange;
@@ -1127,14 +1126,14 @@
                 // relevantly affected without the parent channel already having been.
             }
 
-            if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd
+            if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
                     || channel.getImportance() != updatedChannel.getImportance()) {
                 needsDndChange = true;
                 changed = true;
             }
         }
         if (needsDndChange) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
         if (changed) {
             updateConfig();
@@ -1321,7 +1320,7 @@
             }
         }
         if (channelBypassedDnd) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
         return deletedChannel;
     }
@@ -1538,7 +1537,7 @@
             }
         }
         if (groupBypassedDnd) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
         return deletedChannels;
     }
@@ -1685,8 +1684,8 @@
                 }
             }
         }
-        if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) {
-            updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+        if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
+            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
         return deletedChannelIds;
     }
@@ -1788,21 +1787,28 @@
     }
 
     /**
-     * Syncs {@link #mAreChannelsBypassingDnd} with the current user's notification policy before
-     * updating
+     * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+     * policy before updating. Must be called:
+     * <ul>
+     *     <li>On system init, after channels and DND configurations are loaded.</li>
+     *     <li>When the current user changes, after the corresponding DND config is loaded.</li>
+     * </ul>
      */
-    private void syncChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
-        mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
-                & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
+    void syncChannelsBypassingDnd() {
+        mCurrentUserHasChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
+                & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
 
-        updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+        updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+                /* fromSystemOrSystemUi= */ true);
     }
 
     /**
-     * Updates the user's NotificationPolicy based on whether the current userId
-     * has channels bypassing DND
+     * Updates the user's NotificationPolicy based on whether the current userId has channels
+     * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
+     * when the current user is switched.
      */
-    private void updateChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
+    private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+            boolean fromSystemOrSystemUi) {
         ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
 
         final int currentUserId = getCurrentUser();
@@ -1817,7 +1823,7 @@
 
                 for (NotificationChannel channel : r.channels.values()) {
                     if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
-                        candidatePkgs.add(new Pair(r.pkg, r.uid));
+                        candidatePkgs.add(new Pair<>(r.pkg, r.uid));
                         break;
                     }
                 }
@@ -1830,9 +1836,9 @@
             }
         }
         boolean haveBypassingApps = candidatePkgs.size() > 0;
-        if (mAreChannelsBypassingDnd != haveBypassingApps) {
-            mAreChannelsBypassingDnd = haveBypassingApps;
-            updateZenPolicy(mAreChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
+        if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
+            mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+            updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
         }
     }
 
@@ -1869,7 +1875,7 @@
     }
 
     public boolean areChannelsBypassingDnd() {
-        return mAreChannelsBypassingDnd;
+        return mCurrentUserHasChannelsBypassingDnd;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/permission/TEST_MAPPING b/services/core/java/com/android/server/pm/permission/TEST_MAPPING
index 579d4e3..b2dcf37 100644
--- a/services/core/java/com/android/server/pm/permission/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/permission/TEST_MAPPING
@@ -4,6 +4,9 @@
             "name": "CtsPermissionTestCases",
             "options": [
                 {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                },
+                {
                     "include-filter": "android.permission.cts.BackgroundPermissionsTest"
                 },
                 {
@@ -29,6 +32,9 @@
             "name": "CtsPermissionPolicyTestCases",
             "options": [
                 {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                },
+                {
                     "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
                 },
                 {
@@ -59,6 +65,29 @@
             "options": [
                 {
                     "include-filter": "android.permission.cts.PermissionUpdateListenerTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.BackgroundPermissionsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SplitPermissionTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.PermissionFlagsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SharedUidPermissionsTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsPermissionPolicyTestCases",
+            "options": [
+                {
+                    "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest"
                 }
             ]
         }
diff --git a/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java b/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java
index a2177e8..0bb969f 100644
--- a/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java
+++ b/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java
@@ -17,13 +17,13 @@
 
 import android.content.pm.parsing.ApkLiteParseUtils;
 import android.content.pm.parsing.PackageLite;
-import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
-import com.android.server.pm.pkg.parsing.ParsingPackageUtils.ParseFlags;
 import android.content.res.ApkAssets;
 import android.content.res.AssetManager;
 import android.os.Build;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
+import com.android.server.pm.pkg.parsing.ParsingPackageUtils.ParseFlags;
 
 import libcore.io.IoUtils;
 
@@ -82,8 +82,8 @@
         }
 
         AssetManager assets = new AssetManager();
-        assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                Build.VERSION.RESOURCES_SDK_INT);
+        assets.setConfiguration(0, 0, null, new String[0], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, Build.VERSION.RESOURCES_SDK_INT);
         assets.setApkAssets(apkAssets, false /*invalidateCaches*/);
 
         mCachedAssetManager = assets;
diff --git a/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java b/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java
index 1a8c1996..56d92fb 100644
--- a/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java
+++ b/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java
@@ -80,8 +80,8 @@
 
     private static AssetManager createAssetManagerWithAssets(ApkAssets[] apkAssets) {
         final AssetManager assets = new AssetManager();
-        assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                Build.VERSION.RESOURCES_SDK_INT);
+        assets.setConfiguration(0, 0, null, new String[0], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, Build.VERSION.RESOURCES_SDK_INT);
         assets.setApkAssets(apkAssets, false /*invalidateCaches*/);
         return assets;
     }
diff --git a/services/core/java/com/android/server/policy/TEST_MAPPING b/services/core/java/com/android/server/policy/TEST_MAPPING
index 9f1cb1a..819a82c 100644
--- a/services/core/java/com/android/server/policy/TEST_MAPPING
+++ b/services/core/java/com/android/server/policy/TEST_MAPPING
@@ -32,6 +32,9 @@
       "name": "CtsPermissionPolicyTestCases",
       "options": [
         {
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
           "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
         },
         {
@@ -46,6 +49,9 @@
       "name": "CtsPermissionTestCases",
       "options": [
         {
+          "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
           "include-filter": "android.permission.cts.SplitPermissionTest"
         },
         {
@@ -78,6 +84,31 @@
           "include-filter": "com.android.server.policy."
         }
       ]
+    },
+    {
+      "name": "CtsPermissionPolicyTestCases",
+      "options": [
+        {
+          "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
+        },
+        {
+          "include-filter": "android.permissionpolicy.cts.RestrictedStoragePermissionSharedUidTest"
+        },
+        {
+          "include-filter": "android.permissionpolicy.cts.RestrictedStoragePermissionTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsPermissionTestCases",
+      "options": [
+        {
+          "include-filter": "android.permission.cts.SplitPermissionTest"
+        },
+        {
+          "include-filter": "android.permission.cts.BackgroundPermissionsTest"
+        }
+      ]
     }
   ]
 }
diff --git a/services/core/java/com/android/server/power/TEST_MAPPING b/services/core/java/com/android/server/power/TEST_MAPPING
index fbfe291..8374997 100644
--- a/services/core/java/com/android/server/power/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/TEST_MAPPING
@@ -24,6 +24,15 @@
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"}
       ]
+    },
+    {
+      "name": "PowerServiceTests",
+      "options": [
+        {"include-filter": "com.android.server.power"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
     }
   ],
   "postsubmit": [
@@ -42,6 +51,14 @@
         {"include-filter": "com.android.server.power"},
         {"exclude-filter": "com.android.server.power.BatteryStatsTests"}
       ]
+    },
+    {
+      "name": "PowerServiceTests",
+      "options": [
+        {"include-filter": "com.android.server.power"},
+        {"exclude-filter": "com.android.server.power.BatteryStatsTests"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
     }
   ]
 }
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 308ce4f..19dd0b2 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -21,9 +21,16 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.util.Slog;
+import android.util.SparseArray;
 import android.view.HapticFeedbackConstants;
 
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback.
@@ -31,6 +38,8 @@
  * @hide
  */
 public final class HapticFeedbackVibrationProvider {
+    private static final String TAG = "HapticFeedbackVibrationProvider";
+
     private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
     private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES =
@@ -42,15 +51,41 @@
     private final boolean mHapticTextHandleEnabled;
     // Vibrator effect for haptic feedback during boot when safe mode is enabled.
     private final VibrationEffect mSafeModeEnabledVibrationEffect;
+    // Haptic feedback vibration customizations specific to the device.
+    // If present and valid, a vibration here will be used for an effect.
+    // Otherwise, the system's default vibration will be used.
+    @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations;
 
     /** @hide */
     public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) {
+        this(res, vibrator, loadHapticCustomizations(res));
+    }
+
+    /** @hide */
+    @VisibleForTesting HapticFeedbackVibrationProvider(
+            Resources res,
+            Vibrator vibrator,
+            @Nullable SparseArray<VibrationEffect> hapticCustomizations) {
         mVibrator = vibrator;
         mHapticTextHandleEnabled = res.getBoolean(
                 com.android.internal.R.bool.config_enableHapticTextHandle);
+
+        if (hapticCustomizations != null) {
+            // Clean up the customizations to remove vibrations which may not ever be used due to
+            // Vibrator properties or other device configurations.
+            removeUnsupportedVibrations(hapticCustomizations, vibrator);
+            if (hapticCustomizations.size() == 0) {
+                hapticCustomizations = null;
+            }
+        }
+        mHapticCustomizations = hapticCustomizations;
+
         mSafeModeEnabledVibrationEffect =
-                VibrationSettings.createEffectFromResource(
-                        res, com.android.internal.R.array.config_safeModeEnabledVibePattern);
+                effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED)
+                        ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED)
+                        : VibrationSettings.createEffectFromResource(
+                                res,
+                                com.android.internal.R.array.config_safeModeEnabledVibePattern);
     }
 
     /**
@@ -68,7 +103,7 @@
             case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE:
             case HapticFeedbackConstants.SCROLL_TICK:
             case HapticFeedbackConstants.SEGMENT_TICK:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+                return getVibration(effectId, VibrationEffect.EFFECT_TICK);
 
             case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
                 if (!mHapticTextHandleEnabled) {
@@ -77,13 +112,16 @@
                 // fallthrough
             case HapticFeedbackConstants.CLOCK_TICK:
             case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK);
+                return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK);
 
             case HapticFeedbackConstants.KEYBOARD_RELEASE:
             case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
             case HapticFeedbackConstants.ENTRY_BUMP:
             case HapticFeedbackConstants.DRAG_CROSSING:
-                return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+                return getVibration(
+                        effectId,
+                        VibrationEffect.EFFECT_TICK,
+                        /* fallbackForPredefinedEffect= */ false);
 
             case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
             case HapticFeedbackConstants.VIRTUAL_KEY:
@@ -93,46 +131,42 @@
             case HapticFeedbackConstants.GESTURE_START:
             case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
             case HapticFeedbackConstants.SCROLL_LIMIT:
-                return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+                return getVibration(effectId, VibrationEffect.EFFECT_CLICK);
 
             case HapticFeedbackConstants.LONG_PRESS:
             case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON:
             case HapticFeedbackConstants.DRAG_START:
             case HapticFeedbackConstants.EDGE_SQUEEZE:
-                return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+                return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK);
 
             case HapticFeedbackConstants.REJECT:
-                return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+                return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK);
 
             case HapticFeedbackConstants.SAFE_MODE_ENABLED:
                 return mSafeModeEnabledVibrationEffect;
 
             case HapticFeedbackConstants.ASSISTANT_BUTTON:
-                if (mVibrator.areAllPrimitivesSupported(
-                        VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
-                        VibrationEffect.Composition.PRIMITIVE_TICK)) {
-                    // quiet ramp, short pause, then sharp tick
-                    return VibrationEffect.startComposition()
-                            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
-                            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
-                            .compose();
-                }
-                // fallback for devices without composition support
-                return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+                return getAssistantButtonVibration();
 
             case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE:
-                return getScaledPrimitiveOrElseEffect(
-                        VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f,
+                return getVibration(
+                        effectId,
+                        VibrationEffect.Composition.PRIMITIVE_TICK,
+                        /* primitiveScale= */ 0.4f,
                         VibrationEffect.EFFECT_TEXTURE_TICK);
 
             case HapticFeedbackConstants.TOGGLE_ON:
-                return getScaledPrimitiveOrElseEffect(
-                        VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f,
+                return getVibration(
+                        effectId,
+                        VibrationEffect.Composition.PRIMITIVE_TICK,
+                        /* primitiveScale= */ 0.5f,
                         VibrationEffect.EFFECT_TICK);
 
             case HapticFeedbackConstants.TOGGLE_OFF:
-                return getScaledPrimitiveOrElseEffect(
-                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 0.2f,
+                return getVibration(
+                        effectId,
+                        VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+                        /* primitiveScale= */ 0.2f,
                         VibrationEffect.EFFECT_TEXTURE_TICK);
 
             case HapticFeedbackConstants.NO_HAPTICS:
@@ -181,14 +215,100 @@
         pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled);
     }
 
-    private VibrationEffect getScaledPrimitiveOrElseEffect(
-            int primitiveId, float scale, int elseEffectId) {
+    private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) {
+        return getVibration(
+                effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true);
+    }
+
+    /**
+     * Returns the customized vibration for {@code hapticFeedbackId}, or
+     * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic
+     * feedback.
+     *
+     * <p>If a customization does not exist and the default predefined effect is to be returned,
+     * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback
+     * to a generic pattern if the predefined effect is not hardware supported.
+     *
+     * @see VibrationEffect#get(int, boolean)
+     */
+    private VibrationEffect getVibration(
+            int hapticFeedbackId,
+            int predefinedVibrationEffectId,
+            boolean fallbackForPredefinedEffect) {
+        if (effectHasCustomization(hapticFeedbackId)) {
+            return mHapticCustomizations.get(hapticFeedbackId);
+        }
+        return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect);
+    }
+
+    /**
+     * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if
+     * a customization does not exist for the ID.
+     *
+     * <p>The fallback will be a primitive composition formed of {@code primitiveId} and
+     * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined
+     * vibration of {@code elsePredefinedVibrationEffectId}.
+     */
+    private VibrationEffect getVibration(
+            int hapticFeedbackId,
+            int primitiveId,
+            float primitiveScale,
+            int elsePredefinedVibrationEffectId) {
+        if (effectHasCustomization(hapticFeedbackId)) {
+            return mHapticCustomizations.get(hapticFeedbackId);
+        }
         if (mVibrator.areAllPrimitivesSupported(primitiveId)) {
             return VibrationEffect.startComposition()
-                    .addPrimitive(primitiveId, scale)
+                    .addPrimitive(primitiveId, primitiveScale)
                     .compose();
         } else {
-            return VibrationEffect.get(elseEffectId);
+            return VibrationEffect.get(elsePredefinedVibrationEffectId);
+        }
+    }
+
+    private VibrationEffect getAssistantButtonVibration() {
+        if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) {
+            return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON);
+        }
+        if (mVibrator.areAllPrimitivesSupported(
+                VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+                VibrationEffect.Composition.PRIMITIVE_TICK)) {
+            // quiet ramp, short pause, then sharp tick
+            return VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
+                    .compose();
+        }
+        // fallback for devices without composition support
+        return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+    }
+
+    private boolean effectHasCustomization(int effectId) {
+        return mHapticCustomizations != null && mHapticCustomizations.contains(effectId);
+    }
+
+    @Nullable
+    private static SparseArray<VibrationEffect> loadHapticCustomizations(Resources res) {
+        try {
+            return HapticFeedbackCustomization.loadVibrations(res);
+        } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) {
+            Slog.e(TAG, "Unable to load haptic customizations.", e);
+            return null;
+        }
+    }
+
+    private static void removeUnsupportedVibrations(
+            SparseArray<VibrationEffect> customizations, Vibrator vibrator) {
+        Set<Integer> keysToRemove = new HashSet<>();
+        for (int i = 0; i < customizations.size(); i++) {
+            int key = customizations.keyAt(i);
+            if (!vibrator.areVibrationFeaturesSupported(customizations.get(key))) {
+                keysToRemove.add(key);
+            }
+        }
+
+        for (int key : keysToRemove) {
+            customizations.remove(key);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 0a62c88..0c1c1d4 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -3110,7 +3110,18 @@
         } else {
             TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
             if (candidateTf == null) {
-                final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */);
+                // Puts the activity on the top-most non-isolated navigation TF, unless the
+                // activity is launched from the same TF.
+                final TaskFragment sourceTaskFragment =
+                        mSourceRecord != null ? mSourceRecord.getTaskFragment() : null;
+                final ActivityRecord top = task.getActivity(r -> {
+                    if (!r.canBeTopRunning()) {
+                        return false;
+                    }
+                    final TaskFragment taskFragment = r.getTaskFragment();
+                    return !taskFragment.isIsolatedNav() || (sourceTaskFragment != null
+                            && sourceTaskFragment == taskFragment);
+                });
                 if (top != null) {
                     candidateTf = top.getTaskFragment();
                 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index aeaf783..2c866ab 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -311,6 +311,7 @@
  * {@hide}
  */
 public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
+    private static final String GRAMMATICAL_GENDER_PROPERTY = "persist.sys.grammatical_gender";
     private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityTaskManagerService" : TAG_ATM;
     static final String TAG_ROOT_TASK = TAG + POSTFIX_ROOT_TASK;
     static final String TAG_SWITCH = TAG + POSTFIX_SWITCH;
@@ -928,6 +929,14 @@
             configuration.setLayoutDirection(configuration.locale);
         }
 
+        // Retrieve the grammatical gender from system property, set it into configuration which
+        // will get updated later if the grammatical gender raw value of current configuration is
+        // {@link Configuration#GRAMMATICAL_GENDER_UNDEFINED}.
+        if (configuration.getGrammaticalGenderRaw() == Configuration.GRAMMATICAL_GENDER_UNDEFINED) {
+            configuration.setGrammaticalGender(SystemProperties.getInt(GRAMMATICAL_GENDER_PROPERTY,
+                    Configuration.GRAMMATICAL_GENDER_UNDEFINED));
+        }
+
         synchronized (mGlobalLock) {
             mForceResizableActivities = forceResizable;
             mDevEnableNonResizableMultiWindow = devEnableNonResizableMultiWindow;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index d675753..467937c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5422,8 +5422,10 @@
             // Attach the SystemUiContext to this DisplayContent the get latest configuration.
             // Note that the SystemUiContext will be removed automatically if this DisplayContent
             // is detached.
+            final WindowProcessController wpc = mAtmService.getProcessController(
+                    getDisplayUiContext().getIApplicationThread());
             mWmService.mWindowContextListenerController.registerWindowContainerListener(
-                    getDisplayUiContext().getWindowContextToken(), this, SYSTEM_UID,
+                    wpc, getDisplayUiContext().getWindowContextToken(), this, SYSTEM_UID,
                     INVALID_WINDOW_TYPE, null /* options */);
         }
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 92c0987..57ce368 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -329,6 +329,15 @@
      */
     private boolean mDelayLastActivityRemoval;
 
+    /**
+     * Whether the activity navigation should be isolated. That is, Activities cannot be launched
+     * on an isolated TaskFragment, unless the activity is launched from an Activity in the same
+     * isolated TaskFragment, or explicitly requested to be launched to.
+     * <p>
+     * Note that only an embedded TaskFragment can be isolated.
+     */
+    private boolean mIsolatedNav;
+
     final Point mLastSurfaceSize = new Point();
 
     private final Rect mTmpBounds = new Rect();
@@ -481,6 +490,19 @@
         return mAnimationParams;
     }
 
+    /** @see #mIsolatedNav */
+    void setIsolatedNav(boolean isolatedNav) {
+        if (!isEmbedded()) {
+            return;
+        }
+        mIsolatedNav = isolatedNav;
+    }
+
+    /** @see #mIsolatedNav */
+    boolean isIsolatedNav() {
+        return isEmbedded() && mIsolatedNav;
+    }
+
     TaskFragment getAdjacentTaskFragment() {
         return mAdjacentTaskFragment;
     }
@@ -3034,7 +3056,8 @@
     @Override
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         super.dump(pw, prefix, dumpAll);
-        pw.println(prefix + "bounds=" + getBounds().toShortString());
+        pw.println(prefix + "bounds=" + getBounds().toShortString()
+                + (mIsolatedNav ? ", isolatedNav" : ""));
         final String doublePrefix = prefix + "  ";
         for (int i = mChildren.size() - 1; i >= 0; i--) {
             final WindowContainer<?> child = mChildren.get(i);
diff --git a/services/core/java/com/android/server/wm/WindowContextListenerController.java b/services/core/java/com/android/server/wm/WindowContextListenerController.java
index 4025cbf..a59c230 100644
--- a/services/core/java/com/android/server/wm/WindowContextListenerController.java
+++ b/services/core/java/com/android/server/wm/WindowContextListenerController.java
@@ -69,22 +69,24 @@
     final ArrayMap<IBinder, WindowContextListenerImpl> mListeners = new ArrayMap<>();
 
     /**
-     * @see #registerWindowContainerListener(IBinder, WindowContainer, int, int, Bundle, boolean)
+     * @see #registerWindowContainerListener(WindowProcessController, IBinder, WindowContainer, int,
+     * int, Bundle, boolean)
      */
-    void registerWindowContainerListener(@NonNull IBinder clientToken,
-            @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type,
-            @Nullable Bundle options) {
-        registerWindowContainerListener(clientToken, container, ownerUid, type, options,
+    void registerWindowContainerListener(@NonNull WindowProcessController wpc,
+            @NonNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid,
+            @WindowType int type, @Nullable Bundle options) {
+        registerWindowContainerListener(wpc, clientToken, container, ownerUid, type, options,
                 true /* shouDispatchConfigWhenRegistering */);
     }
 
     /**
      * Registers the listener to a {@code container} which is associated with
-     * a {@code clientToken}, which is a {@link android.window.WindowContext} representation. If the
+     * a {@code clientToken}, which is a {@link WindowContext} representation. If the
      * listener associated with {@code clientToken} hasn't been initialized yet, create one
      * {@link WindowContextListenerImpl}. Otherwise, the listener associated with
      * {@code clientToken} switches to listen to the {@code container}.
      *
+     * @param wpc the process that we should send the window configuration change to
      * @param clientToken the token to associate with the listener
      * @param container the {@link WindowContainer} which the listener is going to listen to.
      * @param ownerUid the caller UID
@@ -94,19 +96,32 @@
      *                {@code container}'s config will dispatch to the client side when
      *                registering the {@link WindowContextListenerImpl}
      */
-    void registerWindowContainerListener(@NonNull IBinder clientToken,
-            @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type,
-            @Nullable Bundle options, boolean shouDispatchConfigWhenRegistering) {
+    void registerWindowContainerListener(@NonNull WindowProcessController wpc,
+            @NonNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid,
+            @WindowType int type, @Nullable Bundle options,
+            boolean shouDispatchConfigWhenRegistering) {
         WindowContextListenerImpl listener = mListeners.get(clientToken);
         if (listener == null) {
-            listener = new WindowContextListenerImpl(clientToken, container, ownerUid, type,
+            listener = new WindowContextListenerImpl(wpc, clientToken, container, ownerUid, type,
                     options);
             listener.register(shouDispatchConfigWhenRegistering);
         } else {
-            listener.updateContainer(container);
+            updateContainerForWindowContextListener(clientToken, container);
         }
     }
 
+    /**
+     * Updates the {@link WindowContainer} that an existing {@link WindowContext} is listening to.
+     */
+    void updateContainerForWindowContextListener(@NonNull IBinder clientToken,
+            @NonNull WindowContainer<?> container) {
+        final WindowContextListenerImpl listener = mListeners.get(clientToken);
+        if (listener == null) {
+            throw new IllegalArgumentException("Can't find listener for " + clientToken);
+        }
+        listener.updateContainer(container);
+    }
+
     void unregisterWindowContainerListener(IBinder clientToken) {
         final WindowContextListenerImpl listener = mListeners.get(clientToken);
         // Listeners may be removed earlier. An example is the display where the listener is
@@ -189,9 +204,13 @@
 
     @VisibleForTesting
     class WindowContextListenerImpl implements WindowContainerListener {
-        @NonNull private final IWindowToken mClientToken;
+        @NonNull
+        private final WindowProcessController mWpc;
+        @NonNull
+        private final IWindowToken mClientToken;
         private final int mOwnerUid;
-        @NonNull private WindowContainer<?> mContainer;
+        @NonNull
+        private WindowContainer<?> mContainer;
         /**
          * The options from {@link Context#createWindowContext(int, Bundle)}.
          * <p>It can be used for choosing the {@link DisplayArea} where the window context
@@ -207,8 +226,10 @@
 
         private boolean mHasPendingConfiguration;
 
-        private WindowContextListenerImpl(IBinder clientToken, WindowContainer<?> container,
+        private WindowContextListenerImpl(@NonNull WindowProcessController wpc,
+                @NonNull IBinder clientToken, @NonNull WindowContainer<?> container,
                 int ownerUid, @WindowType int type, @Nullable Bundle options) {
+            mWpc = Objects.requireNonNull(wpc);
             mClientToken = IWindowToken.Stub.asInterface(clientToken);
             mContainer = Objects.requireNonNull(container);
             mOwnerUid = ownerUid;
@@ -308,6 +329,7 @@
             mLastReportedDisplay = displayId;
 
             try {
+                // TODO(b/290876897): migrate to dispatch through wpc
                 mClientToken.onConfigurationChanged(config, displayId);
             } catch (RemoteException e) {
                 ProtoLog.w(WM_ERROR, "Could not report config changes to the window token client.");
@@ -337,6 +359,7 @@
             }
             mDeathRecipient.unlinkToDeath();
             try {
+                // TODO(b/290876897): migrate to dispatch through wpc
                 mClientToken.onWindowTokenRemoved();
             } catch (RemoteException e) {
                 ProtoLog.w(WM_ERROR, "Could not report token removal to the window token client.");
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 15b15a8..2a28010 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1698,8 +1698,8 @@
                         return WindowManagerGlobal.ADD_INVALID_TYPE;
                     }
                 } else {
-                    mWindowContextListenerController.registerWindowContainerListener(
-                            windowContextToken, token, callingUid, type, options);
+                    mWindowContextListenerController.updateContainerForWindowContextListener(
+                            windowContextToken, token);
                 }
             }
 
@@ -2744,10 +2744,17 @@
         Objects.requireNonNull(clientToken);
         final boolean callerCanManageAppTokens = checkCallingPermission(MANAGE_APP_TOKENS,
                 "attachWindowContextToDisplayArea", false /* printLog */);
+        final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
+                final WindowProcessController wpc = mAtmService.getProcessController(appThread);
+                if (wpc == null) {
+                    ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayArea: calling from"
+                            + " non-existing process pid=%d uid=%d", callingPid, callingUid);
+                    return null;
+                }
                 final DisplayContent dc = mRoot.getDisplayContentOrCreate(displayId);
                 if (dc == null) {
                     ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayArea: trying to attach"
@@ -2758,8 +2765,9 @@
                 // the feature b/155340867 is completed.
                 final DisplayArea<?> da = dc.findAreaForWindowType(type, options,
                         callerCanManageAppTokens, false /* roundedCornerOverlay */);
-                mWindowContextListenerController.registerWindowContainerListener(clientToken, da,
-                        callingUid, type, options, false /* shouDispatchConfigWhenRegistering */);
+                mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken,
+                        da, callingUid, type, options,
+                        false /* shouDispatchConfigWhenRegistering */);
                 return da.getConfiguration();
             }
         } finally {
@@ -2773,10 +2781,17 @@
             @NonNull IBinder clientToken, int displayId) {
         Objects.requireNonNull(appThread);
         Objects.requireNonNull(clientToken);
+        final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
+                final WindowProcessController wpc = mAtmService.getProcessController(appThread);
+                if (wpc == null) {
+                    ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayContent: calling from"
+                            + " non-existing process pid=%d uid=%d", callingPid, callingUid);
+                    return null;
+                }
                 // We use "getDisplayContent" instead of "getDisplayContentOrCreate" because
                 // this method may be called in DisplayPolicy's constructor and may cause
                 // infinite loop. In this scenario, we early return here and switch to do the
@@ -2793,8 +2808,8 @@
                     return null;
                 }
 
-                mWindowContextListenerController.registerWindowContainerListener(clientToken, dc,
-                        callingUid, INVALID_WINDOW_TYPE, null /* options */,
+                mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken,
+                        dc, callingUid, INVALID_WINDOW_TYPE, null /* options */,
                         false /* shouDispatchConfigWhenRegistering */);
                 return dc.getConfiguration();
             }
@@ -2811,10 +2826,17 @@
         Objects.requireNonNull(token);
         final boolean callerCanManageAppTokens = checkCallingPermission(MANAGE_APP_TOKENS,
                 "attachWindowContextToWindowToken", false /* printLog */);
+        final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
+                final WindowProcessController wpc = mAtmService.getProcessController(appThread);
+                if (wpc == null) {
+                    ProtoLog.w(WM_ERROR, "attachWindowContextToWindowToken: calling from"
+                            + " non-existing process pid=%d uid=%d", callingPid, callingUid);
+                    return;
+                }
                 final WindowToken windowToken = mRoot.getWindowToken(token);
                 if (windowToken == null) {
                     ProtoLog.w(WM_ERROR, "Then token:%s is invalid. It might be "
@@ -2835,7 +2857,7 @@
                         callerCanManageAppTokens, callingUid)) {
                     return;
                 }
-                mWindowContextListenerController.registerWindowContainerListener(clientToken,
+                mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken,
                         windowToken, callingUid, windowToken.windowType, windowToken.mOptions);
             }
         } finally {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 164c8b0..a84749a 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -30,6 +30,7 @@
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
 import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN;
@@ -1356,6 +1357,11 @@
                 }
                 break;
             }
+            case OP_TYPE_SET_ISOLATED_NAVIGATION: {
+                final boolean isolatedNav = operation.isIsolatedNav();
+                taskFragment.setIsolatedNav(isolatedNav);
+                break;
+            }
         }
         return effects;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 77f8de5..c323a7f 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -241,7 +241,6 @@
 import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
 import static android.provider.Telephony.Carriers.INVALID_APN_ID;
 import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
-
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -540,6 +539,8 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
@@ -18825,10 +18826,16 @@
         });
     }
 
+    ThreadPoolExecutor calculateHasIncompatibleAccountsExecutor = new ThreadPoolExecutor(
+            1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+
     @Override
     public void calculateHasIncompatibleAccounts() {
+        if (calculateHasIncompatibleAccountsExecutor.getQueue().size() > 1) {
+            return;
+        }
         new CalculateHasIncompatibleAccountsTask().executeOnExecutor(
-                AsyncTask.THREAD_POOL_EXECUTOR, null);
+                calculateHasIncompatibleAccountsExecutor, null);
     }
 
     @Nullable
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ee4bc12..6a2d4dc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2447,8 +2447,9 @@
                 t.traceBegin("StartCompanionDeviceManager");
                 mSystemServiceManager.startService(COMPANION_DEVICE_MANAGER_SERVICE_CLASS);
                 t.traceEnd();
+            }
 
-                // VirtualDeviceManager depends on CDM to control the associations.
+            if (context.getResources().getBoolean(R.bool.config_enableVirtualDeviceManager)) {
                 t.traceBegin("StartVirtualDeviceManager");
                 mSystemServiceManager.startService(VIRTUAL_DEVICE_MANAGER_SERVICE_CLASS);
                 t.traceEnd();
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index 5e7dd59..c0cfa53 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -31,6 +31,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.media.MediaMetrics;
 import android.media.midi.IBluetoothMidiService;
@@ -43,6 +44,7 @@
 import android.media.midi.MidiDeviceService;
 import android.media.midi.MidiDeviceStatus;
 import android.media.midi.MidiManager;
+import android.media.midi.MidiUmpDeviceService;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -580,10 +582,14 @@
                         intent.setComponent(new ComponentName(
                                 MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE,
                                 MidiManager.BLUETOOTH_MIDI_SERVICE_CLASS));
-                    } else {
+                    } else if (!isUmpDevice(mDeviceInfo)) {
                         intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
                         intent.setComponent(
                                 new ComponentName(mServiceInfo.packageName, mServiceInfo.name));
+                    } else {
+                        intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE);
+                        intent.setComponent(
+                                new ComponentName(mServiceInfo.packageName, mServiceInfo.name));
                     }
 
                     if (!mContext.bindServiceAsUser(intent, mServiceConnection,
@@ -696,8 +702,8 @@
                             isDeviceDisconnected ? "true" : "false")
                     .set(MediaMetrics.Property.IS_SHARED,
                             !mDeviceInfo.isPrivate() ? "true" : "false")
-                    .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP, mDeviceInfo.getDefaultProtocol()
-                             != MidiDeviceInfo.PROTOCOL_UNKNOWN ? "true" : "false")
+                    .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP,
+                            isUmpDevice(mDeviceInfo) ? "true" : "false")
                     .set(MediaMetrics.Property.USING_ALSA, mDeviceInfo.getProperties().get(
                             MidiDeviceInfo.PROPERTY_ALSA_CARD) != null ? "true" : "false")
                     .set(MediaMetrics.Property.EVENT, "deviceClosed")
@@ -971,19 +977,37 @@
     private void onStartOrUnlockUser(TargetUser user, boolean matchDirectBootUnaware) {
         Log.d(TAG, "onStartOrUnlockUser " + user.getUserIdentifier() + " matchDirectBootUnaware: "
                 + matchDirectBootUnaware);
-        Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
         int resolveFlags = PackageManager.GET_META_DATA;
         if (matchDirectBootUnaware) {
             resolveFlags |= PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
         }
-        List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
-                resolveFlags, user.getUserIdentifier());
-        if (resolveInfos != null) {
-            int count = resolveInfos.size();
-            for (int i = 0; i < count; i++) {
-                ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
-                if (serviceInfo != null) {
-                    addPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+
+        {
+            Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
+            List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
+                    resolveFlags, user.getUserIdentifier());
+            if (resolveInfos != null) {
+                int count = resolveInfos.size();
+                for (int i = 0; i < count; i++) {
+                    ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+                    if (serviceInfo != null) {
+                        addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+                    }
+                }
+            }
+        }
+
+        {
+            Intent intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE);
+            List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
+                    resolveFlags, user.getUserIdentifier());
+            if (resolveInfos != null) {
+                int count = resolveInfos.size();
+                for (int i = 0; i < count; i++) {
+                    ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+                    if (serviceInfo != null) {
+                        addUmpPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+                    }
                 }
             }
         }
@@ -1057,13 +1081,11 @@
                 if (device.isUidAllowed(uid) && device.isUserIdAllowed(userId)) {
                     // UMP devices have protocols that are not PROTOCOL_UNKNOWN
                     if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) {
-                        if (device.getDeviceInfo().getDefaultProtocol()
-                                != MidiDeviceInfo.PROTOCOL_UNKNOWN) {
+                        if (isUmpDevice(device.getDeviceInfo())) {
                             deviceInfos.add(device.getDeviceInfo());
                         }
                     } else if (transport == MidiManager.TRANSPORT_MIDI_BYTE_STREAM) {
-                        if (device.getDeviceInfo().getDefaultProtocol()
-                                == MidiDeviceInfo.PROTOCOL_UNKNOWN) {
+                        if (!isUmpDevice(device.getDeviceInfo())) {
                             deviceInfos.add(device.getDeviceInfo());
                         }
                     }
@@ -1364,14 +1386,15 @@
         ServiceInfo[] services = info.services;
         if (services == null) return;
         for (int i = 0; i < services.length; i++) {
-            addPackageDeviceServer(services[i], userId);
+            addLegacyPackageDeviceServer(services[i], userId);
+            addUmpPackageDeviceServer(services[i], userId);
         }
     }
 
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
-    private void addPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
-        Log.d(TAG, "addPackageDeviceServer()" + userId);
+    private void addLegacyPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
+        Log.d(TAG, "addLegacyPackageDeviceServer()" + userId);
         XmlResourceParser parser = null;
 
         try {
@@ -1507,6 +1530,128 @@
         }
     }
 
+    @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
+    private void addUmpPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
+        Log.d(TAG, "addUmpPackageDeviceServer()" + userId);
+        XmlResourceParser parser = null;
+
+        try {
+            ComponentName componentName = new ComponentName(serviceInfo.packageName,
+                    serviceInfo.name);
+            int resId = mPackageManager.getProperty(MidiUmpDeviceService.SERVICE_INTERFACE,
+                    componentName).getResourceId();
+            Resources resources = mPackageManager.getResourcesForApplication(
+                    serviceInfo.packageName);
+            parser = resources.getXml(resId);
+            if (parser == null) return;
+
+            // ignore virtual device servers that do not require the correct permission
+            if (!android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE.equals(
+                    serviceInfo.permission)) {
+                Log.w(TAG, "Skipping MIDI device service " + serviceInfo.packageName
+                        + ": it does not require the permission "
+                        + android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE);
+                return;
+            }
+
+            Bundle properties = null;
+            int numPorts = 0;
+            boolean isPrivate = false;
+            ArrayList<String> portNames = new ArrayList<String>();
+
+            while (true) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.END_DOCUMENT) {
+                    break;
+                } else if (eventType == XmlPullParser.START_TAG) {
+                    String tagName = parser.getName();
+                    if ("device".equals(tagName)) {
+                        if (properties != null) {
+                            Log.w(TAG, "nested <device> elements in metadata for "
+                                    + serviceInfo.packageName);
+                            continue;
+                        }
+                        properties = new Bundle();
+                        properties.putParcelable(MidiDeviceInfo.PROPERTY_SERVICE_INFO, serviceInfo);
+                        numPorts = 0;
+                        isPrivate = false;
+
+                        int count = parser.getAttributeCount();
+                        for (int i = 0; i < count; i++) {
+                            String name = parser.getAttributeName(i);
+                            String value = parser.getAttributeValue(i);
+                            if ("private".equals(name)) {
+                                isPrivate = "true".equals(value);
+                            } else {
+                                properties.putString(name, value);
+                            }
+                        }
+                    } else if ("port".equals(tagName)) {
+                        if (properties == null) {
+                            Log.w(TAG, "<port> outside of <device> in metadata for "
+                                    + serviceInfo.packageName);
+                            continue;
+                        }
+                        numPorts++;
+
+                        String portName = null;
+                        int count = parser.getAttributeCount();
+                        for (int i = 0; i < count; i++) {
+                            String name = parser.getAttributeName(i);
+                            String value = parser.getAttributeValue(i);
+                            if ("name".equals(name)) {
+                                portName = value;
+                                break;
+                            }
+                        }
+                        portNames.add(portName);
+                    }
+                } else if (eventType == XmlPullParser.END_TAG) {
+                    String tagName = parser.getName();
+                    if ("device".equals(tagName)) {
+                        if (properties != null) {
+                            if (numPorts == 0) {
+                                Log.w(TAG, "<device> with no ports in metadata for "
+                                        + serviceInfo.packageName);
+                                continue;
+                            }
+
+                            int uid;
+                            try {
+                                ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
+                                        serviceInfo.packageName, 0, userId);
+                                uid = appInfo.uid;
+                            } catch (PackageManager.NameNotFoundException e) {
+                                Log.e(TAG, "could not fetch ApplicationInfo for "
+                                        + serviceInfo.packageName);
+                                continue;
+                            }
+
+                            synchronized (mDevicesByInfo) {
+                                addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL,
+                                        numPorts, numPorts,
+                                        portNames.toArray(EMPTY_STRING_ARRAY),
+                                        portNames.toArray(EMPTY_STRING_ARRAY),
+                                        properties, null, serviceInfo, isPrivate, uid,
+                                        MidiDeviceInfo.PROTOCOL_UMP_MIDI_2_0, userId);
+                            }
+                            // setting properties to null signals that we are no longer
+                            // processing a <device>
+                            properties = null;
+                            portNames.clear();
+                        }
+                    }
+                }
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+                // No such property
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to load component info " + serviceInfo.toString(), e);
+        } finally {
+            if (parser != null) parser.close();
+        }
+    }
+
     private void removePackageDeviceServers(String packageName, int userId) {
         synchronized (mDevicesByInfo) {
             Iterator<Device> iterator = mDevicesByInfo.values().iterator();
@@ -1649,4 +1794,8 @@
     private int getCallingUserId() {
         return UserHandle.getUserId(Binder.getCallingUid());
     }
+
+    private boolean isUmpDevice(MidiDeviceInfo deviceInfo) {
+        return deviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN;
+    }
 }
diff --git a/services/permission/TEST_MAPPING b/services/permission/TEST_MAPPING
index 579d4e3..b2dcf37 100644
--- a/services/permission/TEST_MAPPING
+++ b/services/permission/TEST_MAPPING
@@ -4,6 +4,9 @@
             "name": "CtsPermissionTestCases",
             "options": [
                 {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                },
+                {
                     "include-filter": "android.permission.cts.BackgroundPermissionsTest"
                 },
                 {
@@ -29,6 +32,9 @@
             "name": "CtsPermissionPolicyTestCases",
             "options": [
                 {
+                    "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+                },
+                {
                     "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
                 },
                 {
@@ -59,6 +65,29 @@
             "options": [
                 {
                     "include-filter": "android.permission.cts.PermissionUpdateListenerTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.BackgroundPermissionsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SplitPermissionTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.PermissionFlagsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.SharedUidPermissionsTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsPermissionPolicyTestCases",
+            "options": [
+                {
+                    "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest"
+                },
+                {
+                    "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest"
                 }
             ]
         }
diff --git a/services/tests/displayservicetests/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
index d865519..477860d 100644
--- a/services/tests/displayservicetests/TEST_MAPPING
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -1,13 +1,7 @@
 {
-  "presubmit": [
+  "imports": [
     {
-      "name": "DisplayServiceTests",
-      "options": [
-        {"include-filter": "com.android.server.display"},
-        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-        {"exclude-annotation": "org.junit.Ignore"}
-      ]
+      "path": "frameworks/base/services/core/java/com/android/server/display"
     }
   ]
 }
diff --git a/services/tests/powerservicetests/TEST_MAPPING b/services/tests/powerservicetests/TEST_MAPPING
new file mode 100644
index 0000000..1b534a1
--- /dev/null
+++ b/services/tests/powerservicetests/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+  "imports": [
+    {
+      "path": "frameworks/base/services/core/java/com/android/server/power"
+    }
+  ]
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
index 0037970..8c07b6c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.Validator
 import android.os.Environment
+import android.os.SystemProperties.PROP_VALUE_MAX
 import android.platform.test.annotations.Postsubmit
 import com.android.internal.R
 import com.android.server.pm.PackageManagerService
@@ -28,7 +29,6 @@
 import org.junit.Assert.fail
 import org.junit.Test
 import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 import org.xmlpull.v1.XmlPullParserFactory
 import java.io.ByteArrayInputStream
 import java.io.File
@@ -75,548 +75,370 @@
     }
 
     @Test
-    fun parseBadManifests() {
+    fun parseManifestTag() {
         val tag = "manifest"
-        val prefix = "<manifest $ns>"
-        val suffix = "</manifest>"
-        parseTagBadAttr(tag, "package", 256, )
-        parseTagBadAttr(tag, "android:sharedUserId", 256)
-        parseTagBadAttr(tag, "android:versionName", 4000)
-        parseBadApplicationTags(100, prefix, suffix, tag)
-        parseBadOverlayTags(100, prefix, suffix, tag)
-        parseBadInstrumentationTags(100, prefix, suffix, tag)
-        parseBadPermissionGroupTags(100, prefix, suffix, tag)
-        parseBadPermissionTreeTags(100, prefix, suffix, tag)
-        parseBadSupportsGlTextureTags(100, prefix, suffix, tag)
-        parseBadSupportsScreensTags(100, prefix, suffix, tag)
-        parseBadUsesConfigurationTags(100, prefix, suffix, tag)
-        parseBadUsesPermissionSdk23Tags(100, prefix, suffix, tag)
-        parseBadUsesSdkTags(100, prefix, suffix, tag)
-        parseBadCompatibleScreensTags(200, prefix, suffix, tag)
-        parseBadQueriesTags(200, prefix, suffix, tag)
-        parseBadAttributionTags(400, prefix, suffix, tag)
-        parseBadUsesFeatureTags(400, prefix, suffix, tag)
-        parseBadPermissionTags(2000, prefix, suffix, tag)
-        parseBadUsesPermissionTags(20000, prefix, suffix, tag)
+        validateTagAttr(tag, "package", null, 256)
+        validateTagAttr(tag, "sharedUserId", null, 256)
+        validateTagAttr(tag, "versionName", null, 1024)
+        validateTagCount("application", 100, tag)
+        validateTagCount("overlay", 100, tag)
+        validateTagCount("instrumentation", 100, tag)
+        validateTagCount("permission-group", 100, tag)
+        validateTagCount("permission-tree", 100, tag)
+        validateTagCount("supports-gl-texture", 100, tag)
+        validateTagCount("supports-screens", 100, tag)
+        validateTagCount("uses-configuration", 100, tag)
+        validateTagCount("uses-sdk", 100, tag)
+        validateTagCount("compatible-screens", 200, tag)
+        validateTagCount("queries", 200, tag)
+        validateTagCount("attribution", 400, tag)
+        validateTagCount("uses-feature", 400, tag)
+        validateTagCount("permission", 2000, tag)
+        validateTagCount("uses-permission", 20000, tag)
     }
 
-    private fun parseBadApplicationTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseApplicationTag() {
         val tag = "application"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-
-        parseTagBadAttr(tag, "android:backupAgent", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:manageSpaceActivity", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:process", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:requiredAccountType", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:restrictedAccountType", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:taskAffinity", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-
-        parseBadProfileableTags(100, newPrefix, newSuffix, tag)
-        parseBadUsesNativeLibraryTags(100, newPrefix, newSuffix, tag)
-        parseBadReceiverTags(1000, newPrefix, newSuffix, tag)
-        parseBadServiceTags(1000, newPrefix, newSuffix, tag)
-        parseBadActivityAliasTags(4000, newPrefix, newSuffix, tag)
-        parseBadUsesLibraryTags(4000, newPrefix, newSuffix, tag)
-        parseBadProviderTags(8000, newPrefix, newSuffix, tag)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadActivityTags(40000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "backupAgent",
+            R.styleable.AndroidManifestApplication_backupAgent, 1024)
+        validateTagAttr(tag, "manageSpaceActivity",
+            R.styleable.AndroidManifestApplication_manageSpaceActivity, 1024)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestApplication_name, 1024)
+        validateTagAttr(tag, "permission", R.styleable.AndroidManifestApplication_permission, 1024)
+        validateTagAttr(tag, "process", R.styleable.AndroidManifestApplication_process, 1024)
+        validateTagAttr(tag, "requiredAccountType",
+            R.styleable.AndroidManifestApplication_requiredAccountType, 1024)
+        validateTagAttr(tag, "restrictedAccountType",
+            R.styleable.AndroidManifestApplication_restrictedAccountType, 1024)
+        validateTagAttr(tag, "taskAffinity",
+            R.styleable.AndroidManifestApplication_taskAffinity, 1024)
+        validateTagCount("profileable", 100, tag)
+        validateTagCount("uses-native-library", 100, tag)
+        validateTagCount("receiver", 1000, tag)
+        validateTagCount("service", 1000, tag)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("uses-library", 1000, tag)
+        validateTagCount("activity-alias", 4000, tag)
+        validateTagCount("provider", 8000, tag)
+        validateTagCount("activity", 40000, tag)
     }
 
-    private fun parseBadProfileableTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "profileable"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadUsesNativeLibraryTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseUsesNativeLibraryTag() {
         val tag = "uses-native-library"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesNativeLibrary_name, 1024)
     }
 
-    private fun parseBadReceiverTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseReceiverTag() {
         val tag = "receiver"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:process", 1024, prefix, suffix)
-
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestReceiver_name, 1024)
+        validateTagAttr(tag, "permission", R.styleable.AndroidManifestReceiver_permission, 1024)
+        validateTagAttr(tag, "process", R.styleable.AndroidManifestReceiver_process, 1024)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("intent-filter", 20000, tag)
     }
 
-    private fun parseBadServiceTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseServiceTag() {
         val tag = "service"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:process", 1024, prefix, suffix)
-
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestService_name, 1024)
+        validateTagAttr(tag, "permission", R.styleable.AndroidManifestService_permission, 1024)
+        validateTagAttr(tag, "process", R.styleable.AndroidManifestService_process, 1024)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("intent-filter", 20000, tag)
     }
 
-    private fun parseBadActivityAliasTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseActivityAliasTag() {
         val tag = "activity-alias"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:targetActivity", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestActivityAlias_name, 1024)
+        validateTagAttr(tag, "permission",
+            R.styleable.AndroidManifestActivityAlias_permission, 1024)
+        validateTagAttr(tag, "targetActivity",
+            R.styleable.AndroidManifestActivityAlias_targetActivity, 1024)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("intent-filter", 20000, tag)
     }
 
-    private fun parseBadUsesLibraryTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseUsesLibraryTag() {
         val tag = "uses-library"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesLibrary_name, 1024)
     }
 
-    private fun parseBadActivityTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseActivityTag() {
         val tag = "activity"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:parentActivityName", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:process", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:taskAffinity", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadLayoutTags(1000, newPrefix, newSuffix, tag)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestActivity_name, 1024)
+        validateTagAttr(tag, "parentActivityName",
+            R.styleable.AndroidManifestActivity_parentActivityName, 1024)
+        validateTagAttr(tag, "permission", R.styleable.AndroidManifestActivity_permission, 1024)
+        validateTagAttr(tag, "process", R.styleable.AndroidManifestActivity_process, 1024)
+        validateTagAttr(tag, "taskAffinity", R.styleable.AndroidManifestActivity_taskAffinity, 1024)
+        validateTagCount("layout", 1000, tag)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("intent-filter", 20000, tag)
     }
 
-    private fun parseBadLayoutTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "layout"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadOverlayTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseOverlayTag() {
         val tag = "overlay"
-        parseTagBadAttr(tag, "android:category", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:requiredSystemPropertyName", 32768, prefix, suffix)
-        parseTagBadAttr(tag, "android:requiredSystemPropertyValue", 256, prefix, suffix)
-        parseTagBadAttr(tag, "android:targetPackage", 256, prefix, suffix)
-        parseTagBadAttr(tag, "android:targetName", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "category", R.styleable.AndroidManifestResourceOverlay_category, 1024)
+        validateTagAttr(tag, "requiredSystemPropertyName",
+            R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName, 1024)
+        validateTagAttr(tag, "requiredSystemPropertyValue",
+            R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue, PROP_VALUE_MAX)
+        validateTagAttr(tag, "targetPackage",
+            R.styleable.AndroidManifestResourceOverlay_targetPackage, 256)
+        validateTagAttr(tag, "targetName",
+            R.styleable.AndroidManifestResourceOverlay_targetName, 1024)
     }
 
-    private fun parseBadInstrumentationTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseInstrumentationTag() {
         val tag = "instrumentation"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:targetPackage", 256, prefix, suffix)
-        parseTagBadAttr(tag, "android:targetProcesses", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestInstrumentation_name, 1024)
+        validateTagAttr(tag, "targetPackage",
+            R.styleable.AndroidManifestInstrumentation_targetPackage, 256)
+        validateTagAttr(tag, "targetProcesses",
+            R.styleable.AndroidManifestInstrumentation_targetProcesses, 1024)
     }
 
-    private fun parseBadPermissionGroupTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parsePermissionGroupTag() {
         val tag = "permission-group"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestPermissionGroup_name, 1024)
     }
 
-    private fun parseBadPermissionTreeTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parsePermissionTreeTag() {
         val tag = "permission-tree"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestPermissionTree_name, 1024)
     }
 
-    private fun parseBadSupportsGlTextureTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseSupportsGlTextureTag() {
         val tag = "supports-gl-texture"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", null, 1024)
     }
 
-    private fun parseBadSupportsScreensTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "supports-screens"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadUsesConfigurationTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "uses-configuration"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadUsesPermissionSdk23Tags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseUsesPermissionSdk23Tag() {
         val tag = "uses-permission-sdk-23"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesPermission_name, 1024)
     }
 
-    private fun parseBadUsesSdkTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "uses-sdk"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadCompatibleScreensTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseCompatibleScreensTag() {
         val tag = "compatible-screens"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadScreenTags(4000, newPrefix, newSuffix, tag)
+        validateTagCount("screen", 4000, tag)
     }
 
-    private fun parseBadScreenTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "screen"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadQueriesTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseQueriesTag() {
         val tag = "queries"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadPackageTags(1000, newPrefix, newSuffix, tag)
-        parseBadIntentTags(2000, newPrefix, newSuffix, tag)
-        parseBadProviderTags(8000, newPrefix, newSuffix, tag)
+        validateTagCount("package", 1000, tag)
+        validateTagCount("intent", 2000, tag)
+        validateTagCount("provider", 8000, tag)
     }
 
-    private fun parseBadPackageTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parsePackageTag() {
         val tag = "package"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", null, 1024)
     }
 
-    private fun parseBadIntentTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseIntentTag() {
         val tag = "intent"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadActionTags(20000, newPrefix, newSuffix, tag)
-        parseBadCategoryTags(40000, newPrefix, newSuffix, tag)
-        parseBadDataTags(40000, newPrefix, newSuffix, tag)
+        validateTagCount("action", 20000, tag)
+        validateTagCount("category", 40000, tag)
+        validateTagCount("data", 40000, tag)
     }
 
-    private fun parseBadProviderTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseProviderTag() {
         val tag = "provider"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:process", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:readPermission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:writePermission", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadGrantUriPermissionTags(100, newPrefix, newSuffix, tag)
-        parseBadPathPermissionTags(100, newPrefix, newSuffix, tag)
-        parseBadMetaDataTags(8000, newPrefix, newSuffix, tag)
-        parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestProvider_name, 1024)
+        validateTagAttr(tag, "permission", R.styleable.AndroidManifestProvider_permission, 1024)
+        validateTagAttr(tag, "process", R.styleable.AndroidManifestProvider_process, 1024)
+        validateTagAttr(tag, "readPermission",
+            R.styleable.AndroidManifestProvider_readPermission, 1024)
+        validateTagAttr(tag, "writePermission",
+            R.styleable.AndroidManifestProvider_writePermission, 1024)
+        validateTagCount("grant-uri-permission", 100, tag)
+        validateTagCount("path-permission", 100, tag)
+        validateTagCount("meta-data", 1000, tag)
+        validateTagCount("intent-filter", 20000, tag)
     }
 
-    private fun parseBadGrantUriPermissionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseGrantUriPermissionTag() {
         val tag = "grant-uri-permission"
-        parseTagBadAttr(tag, "android:path", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "path", R.styleable.AndroidManifestGrantUriPermission_path, 4000)
+        validateTagAttr(tag, "pathPrefix",
+            R.styleable.AndroidManifestGrantUriPermission_pathPrefix, 4000)
+        validateTagAttr(tag, "pathPattern",
+            R.styleable.AndroidManifestGrantUriPermission_pathPattern, 4000)
     }
 
-    private fun parseBadPathPermissionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parsePathPermissionTag() {
         val tag = "path-permission"
-        parseTagBadAttr(tag, "android:path", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:readPermission", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:writePermission", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "path", R.styleable.AndroidManifestPathPermission_path, 4000)
+        validateTagAttr(tag, "pathPrefix",
+            R.styleable.AndroidManifestPathPermission_pathPrefix, 4000)
+        validateTagAttr(tag, "pathPattern",
+            R.styleable.AndroidManifestPathPermission_pathPattern, 4000)
+        validateTagAttr(tag, "permission",
+            R.styleable.AndroidManifestPathPermission_permission, 1024)
+        validateTagAttr(tag, "readPermission",
+            R.styleable.AndroidManifestPathPermission_readPermission, 1024)
+        validateTagAttr(tag, "writePermission",
+            R.styleable.AndroidManifestPathPermission_writePermission, 1024)
     }
 
-    private fun parseBadMetaDataTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseMetaDataTag() {
         val tag = "meta-data"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:value", 32768, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestMetaData_name, 1024)
+        validateTagAttr(tag, "value", R.styleable.AndroidManifestMetaData_value, 4000)
     }
 
-    private fun parseBadIntentFilterTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseIntentFilterTag() {
         val tag = "intent-filter"
-        val newPrefix = "$prefix<$tag>"
-        val newSuffix = "</$tag>$suffix"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-        parseBadActionTags(20000, newPrefix, newSuffix, tag)
-        parseBadCategoryTags(40000, newPrefix, newSuffix, tag)
-        parseBadDataTags(40000, newPrefix, newSuffix, tag)
+        validateTagCount("action", 20000, tag)
+        validateTagCount("category", 40000, tag)
+        validateTagCount("data", 40000, tag)
     }
 
-    private fun parseBadActionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseActionTag() {
         val tag = "action"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestAction_name, 1024)
     }
 
-    private fun parseBadCategoryTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseCategoryTag() {
         val tag = "category"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestCategory_name, 1024)
     }
 
-    private fun parseBadDataTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseDataTag() {
         val tag = "data"
-        parseTagBadAttr(tag, "android:scheme", 256, prefix, suffix)
-        parseTagBadAttr(tag, "android:host", 256, prefix, suffix)
-        parseTagBadAttr(tag, "android:path", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathSuffix", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:pathAdvancedPattern", 4000, prefix, suffix)
-        parseTagBadAttr(tag, "android:mimeType", 512, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "scheme", R.styleable.AndroidManifestData_scheme, 256)
+        validateTagAttr(tag, "host", R.styleable.AndroidManifestData_host, 256)
+        validateTagAttr(tag, "path", R.styleable.AndroidManifestData_path, 4000)
+        validateTagAttr(tag, "pathPattern", R.styleable.AndroidManifestData_pathPattern, 4000)
+        validateTagAttr(tag, "pathPrefix", R.styleable.AndroidManifestData_pathPrefix, 4000)
+        validateTagAttr(tag, "pathSuffix", R.styleable.AndroidManifestData_pathSuffix, 4000)
+        validateTagAttr(tag, "pathAdvancedPattern",
+            R.styleable.AndroidManifestData_pathAdvancedPattern, 4000)
+        validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 512)
     }
 
-    private fun parseBadAttributionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
-        val tag = "attribution"
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
-    }
-
-    private fun parseBadUsesFeatureTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseUsesFeatureTag() {
         val tag = "uses-feature"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesFeature_name, 1024)
     }
 
-    private fun parseBadPermissionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parsePermissionTag() {
         val tag = "permission"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseTagBadAttr(tag, "android:permissionGroup", 256, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestPermission_name, 1024)
+        validateTagAttr(tag, "permissionGroup",
+            R.styleable.AndroidManifestPermission_permissionGroup, 256)
     }
 
-    private fun parseBadUsesPermissionTags(
-            maxNum: Int,
-            prefix: String,
-            suffix: String,
-            parentTag: String
-    ) {
+    @Test
+    fun parseUsesPermissionTag() {
         val tag = "uses-permission"
-        parseTagBadAttr(tag, "android:name", 1024, prefix, suffix)
-        parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
+        validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesPermission_name, 1024)
     }
 
-    private fun parseTagBadAttr(
-            tag: String,
-            attrName: String,
-            maxLength: Int,
-            prefix: String = "",
-            suffix: String = ""
+    private fun validateTagAttr(tag: String, name: String, index: Int?, maxLen: Int) {
+        validateTagAttr_shouldPass(tag, name, index, maxLen)
+        validateTagAttr_shouldFail(tag, name, index, maxLen)
+    }
+
+    private fun validateTagAttr_shouldPass(
+        tag: String,
+        name: String,
+        index: Int?,
+        maxLen: Int
     ) {
-        var attrValue = "x".repeat(maxLength)
-        var tagValue = if (tag.equals("manifest")) "$tag $ns" else tag
-        var manifestStr = "$prefix<$tagValue $attrName=\"$attrValue\" />$suffix"
+        val value = "x".repeat(maxLen)
+        val xml = "<$tag $name=\"$value\" />"
+        pullParser.setInput(ByteArrayInputStream(xml.toByteArray()), null)
+        val validator = Validator()
+        pullParser.nextTag()
+        validator.validate(pullParser)
         try {
-            parseManifestStr(manifestStr)
-        } catch (e: XmlPullParserException) {
-            fail("Failed to parse valid <$tag> attribute $attrName with max length of $maxLength:" +
+            validator.validateStrAttr(pullParser, name, value)
+        } catch (e: SecurityException) {
+            fail("Failed to parse valid <$tag> attribute $name with max length of $maxLen:" +
                     " ${e.message}")
         }
-        attrValue = "x".repeat(maxLength + 1)
-        manifestStr = "$prefix<$tagValue $attrName=\"$attrValue\" />$suffix"
-        val e = assertThrows(XmlPullParserException::class.java) {
-            parseManifestStr(manifestStr)
+        if (index != null) {
+            try {
+                validator.validateResStrAttr(pullParser, index, value)
+            } catch (e: SecurityException) {
+                fail("Failed to parse valid <$tag> resource string attribute $name with max" +
+                        " length of $maxLen: ${e.message}")
+            }
         }
-        assertEquals(expectedAttrLengthErrorMsg(attrName.split(":").last(), tag), e.message)
     }
 
-    private fun parseBadTagCount(
-            tag: String,
-            maxNum: Int,
-            parentTag: String,
-            prefix: String,
-            suffix: String
+    private fun validateTagAttr_shouldFail(
+        tag: String,
+        name: String,
+        index: Int?,
+        maxLen: Int
     ) {
-        var tags = "<$tag />".repeat(maxNum)
-        var manifestStr = "$prefix$tags$suffix"
+        val value = "x".repeat(maxLen + 1)
+        val xml = "<$tag $name=\"$value\" />"
+        pullParser.setInput(ByteArrayInputStream(xml.toByteArray()), null)
+        val validator = Validator()
+        pullParser.nextTag()
+        validator.validate(pullParser)
+        val e1 = assertThrows(SecurityException::class.java) {
+            validator.validateStrAttr(pullParser, name, value)
+        }
+        assertEquals(expectedAttrLengthErrorMsg(name, tag), e1.message)
+        if (index != null) {
+            val e2 = assertThrows(SecurityException::class.java) {
+                validator.validateResStrAttr(pullParser, index, value)
+            }
+            assertEquals(expectedResAttrLengthErrorMsg(tag), e2.message)
+        }
+    }
+
+    private fun validateTagCount(tag: String, maxNum: Int, parentTag: String) {
+        validateTagCount_shouldPass(tag, maxNum, parentTag)
+        validateTagCount_shouldFail(tag, maxNum, parentTag)
+    }
+
+    private fun validateTagCount_shouldPass(tag: String, maxNum: Int, parentTag: String) {
+        val tags = "<$tag />".repeat(maxNum)
+        val xml = "<$parentTag>$tags</$parentTag>"
         try {
-            parseManifestStr(manifestStr)
-        } catch (e: XmlPullParserException) {
+            parseXmlStr(xml)
+        } catch (e: SecurityException) {
             fail("Failed to parse <$tag> with max count limit of $maxNum under" +
                     " <$parentTag>: ${e.message}")
         }
-        tags = "<$tag />".repeat(maxNum + 1)
-        manifestStr = "$prefix$tags$suffix"
-        val e = assertThrows(XmlPullParserException::class.java) {
-            parseManifestStr(manifestStr)
+    }
+
+    private fun validateTagCount_shouldFail(tag: String, maxNum: Int, parentTag: String) {
+        val tags = "<$tag />".repeat(maxNum + 1)
+        val xml = "<$parentTag>$tags</$parentTag>"
+        val e = assertThrows(SecurityException::class.java) {
+            parseXmlStr(xml)
         }
         assertEquals(expectedCountErrorMsg(tag, parentTag), e.message)
     }
@@ -624,13 +446,12 @@
     @Test
     fun parseUnexpectedTag_shouldSkip() {
         val host = "x".repeat(256)
-        val dataTags = "<data android:host=\"$host\" />".repeat(2049)
-        val ns = "http://schemas.android.com/apk/res/android"
-        val manifestStr = "<manifest xmlns:android=\"$ns\" package=\"test\">$dataTags</manifest>"
-        parseManifestStr(manifestStr)
+        val dataTags = "<data host=\"$host\" />".repeat(2049)
+        val xml = "<manifest package=\"test\">$dataTags</manifest>"
+        parseXmlStr(xml)
     }
 
-    fun parseManifestStr(manifestStr: String) {
+    fun parseXmlStr(manifestStr: String) {
         pullParser.setInput(ByteArrayInputStream(manifestStr.toByteArray()), null)
         val validator = Validator()
         do {
@@ -647,39 +468,4 @@
 
     fun expectedResAttrLengthErrorMsg(tag: String) =
             "String length limit exceeded for attribute in $tag"
-
-    @Test
-    fun validateResAttrs() {
-        pullParser.setInput(ByteArrayInputStream("<manifest />".toByteArray()), null)
-        pullParser.next()
-        val validator = Validator()
-        validator.validate(pullParser)
-        validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_host,
-                "R.styleable.AndroidManifestData_host", 255)
-        validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_port,
-                "R.styleable.AndroidManifestData_port", 255)
-        validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_scheme,
-                "R.styleable.AndroidManifestData_scheme", 255)
-        validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_mimeType,
-                "R.styleable.AndroidManifestData_mimeType", 512)
-    }
-
-    fun validateResAttr(
-            parser: XmlPullParser,
-            validator: Validator,
-            resId: Int,
-            resIdStr: String,
-            maxLength: Int
-    ) {
-        try {
-            validator.validateAttr(parser, resId, "x".repeat(maxLength))
-        } catch (e: XmlPullParserException) {
-            fail("Failed to parse valid string resource attribute $resIdStr with max length of" +
-                    " $maxLength: ${e.message}")
-        }
-        val e = assertThrows(XmlPullParserException::class.java) {
-            validator.validateAttr(parser, resId, "x".repeat(maxLength + 1))
-        }
-        assertEquals(expectedResAttrLengthErrorMsg("manifest"), e.message)
-    }
-}
\ No newline at end of file
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 0292bca..f1e26d2 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -421,6 +421,7 @@
     @Mock
     MultiRateLimiter mToastRateLimiter;
     BroadcastReceiver mPackageIntentReceiver;
+    BroadcastReceiver mUserSwitchIntentReceiver;
     NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
     TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
 
@@ -651,8 +652,12 @@
                     && filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
                 mPackageIntentReceiver = broadcastReceivers.get(i);
             }
+            if (filter.hasAction(Intent.ACTION_USER_SWITCHED)) {
+                mUserSwitchIntentReceiver = broadcastReceivers.get(i);
+            }
         }
         assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
+        assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
 
         // Pretend the shortcut exists
         List<ShortcutInfo> shortcutInfos = new ArrayList<>();
@@ -12148,6 +12153,21 @@
                 any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER));
     }
 
+    @Test
+    public void onUserSwitched_updatesZenModeAndChannelsBypassingDnd() {
+        Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+        mService.mZenModeHelper = mock(ZenModeHelper.class);
+        mService.setPreferencesHelper(mPreferencesHelper);
+
+        mUserSwitchIntentReceiver.onReceive(mContext, intent);
+
+        InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
+        inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
+        inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+        inOrder.verifyNoMoreInteractions();
+    }
+
     private static <T extends Parcelable> T parcelAndUnparcel(T source,
             Parcelable.Creator<T> creator) {
         Parcel parcel = Parcel.obtain();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index ea670bd..c242554 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -2478,7 +2478,7 @@
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
-
+        mHelper.syncChannelsBypassingDnd();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2509,6 +2509,7 @@
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
+        mHelper.syncChannelsBypassingDnd();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2533,6 +2534,7 @@
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
+        mHelper.syncChannelsBypassingDnd();
 
         // create notification channel that can bypass dnd, but app is blocked
         // expected result: areChannelsBypassingDnd = false
@@ -2587,6 +2589,7 @@
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
+        mHelper.syncChannelsBypassingDnd();
         assertFalse(mHelper.areChannelsBypassingDnd());
         verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean());
         resetZenModeHelper();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
new file mode 100644
index 0000000..cae811e
--- /dev/null
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
+import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
+import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
+import static android.os.VibrationEffect.EFFECT_TICK;
+import static android.view.HapticFeedbackConstants.CLOCK_TICK;
+import static android.view.HapticFeedbackConstants.CONTEXT_CLICK;
+import static android.view.HapticFeedbackConstants.SAFE_MODE_ENABLED;
+import static android.view.HapticFeedbackConstants.TEXT_HANDLE_MOVE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.VibrationEffect;
+import android.os.test.FakeVibrator;
+import android.util.AtomicFile;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+public class HapticFeedbackVibrationProviderTest {
+    @Rule public MockitoRule rule = MockitoJUnit.rule();
+
+    private static final VibrationEffect PRIMITIVE_TICK_EFFECT =
+            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose();
+    private static final VibrationEffect PRIMITIVE_CLICK_EFFECT =
+            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose();
+
+    private Context mContext = InstrumentationRegistry.getContext();
+    private FakeVibrator mVibrator = new FakeVibrator(mContext);
+
+    @Mock private Resources mResourcesMock;
+
+    @Test
+    public void testNonExistentCustomization_useDefault() throws Exception {
+        // No customization file is set.
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+
+        // The customization file specifies no customization.
+        setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>");
+        hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+    }
+
+    @Test
+    public void testExceptionParsingCustomizations_useDefault() throws Exception {
+        setupCustomizationFile("<bad-xml></bad-xml>");
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+    }
+
+    @Test
+    public void testUseValidCustomizedVibration() throws Exception {
+        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
+
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        // The override for `CONTEXT_CLICK` is used.
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+        // `CLOCK_TICK` has no override, so the default vibration is used.
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+    }
+
+    @Test
+    public void testDoNotUseInvalidCustomizedVibration() throws Exception {
+        mockVibratorPrimitiveSupport(new int[] {});
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
+
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        // The override for `CONTEXT_CLICK` is not used because the vibration is not supported.
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+        // `CLOCK_TICK` has no override, so the default vibration is used.
+        assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
+                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+    }
+
+    @Test
+    public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() throws Exception {
+        mockHapticTextSupport(false);
+        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
+
+        // Test with a customization available for `TEXT_HANDLE_MOVE`.
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
+
+        // Test with no customization available for `TEXT_HANDLE_MOVE`.
+        hapticProvider =
+                new HapticFeedbackVibrationProvider(
+                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
+    }
+
+    @Test
+    public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() throws Exception {
+        mockHapticTextSupport(true);
+        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
+
+        // Test with a customization available for `TEXT_HANDLE_MOVE`.
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
+                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+
+        // Test with no customization available for `TEXT_HANDLE_MOVE`.
+        hapticProvider =
+                new HapticFeedbackVibrationProvider(
+                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
+                .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+    }
+
+    @Test
+    public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource()
+                throws Exception {
+        mockSafeModeEnabledVibration(10, 20, 30, 40);
+        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+
+        mockSafeModeEnabledVibration(null);
+        hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+                .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+    }
+
+    @Test
+    public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed()
+                throws Exception {
+        mockSafeModeEnabledVibration(10, 20, 30, 40);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+        // Test with a customization that is not supported by the vibrator.
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+                .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
+
+        // Test with no customizations.
+        hapticProvider =
+                new HapticFeedbackVibrationProvider(
+                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+                .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
+    }
+
+    @Test
+    public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed()
+                throws Exception {
+        mockSafeModeEnabledVibration(null);
+        SparseArray<VibrationEffect> customizations = new SparseArray<>();
+        customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+        // Test with a customization that is not supported by the vibrator.
+        HapticFeedbackVibrationProvider hapticProvider =
+                new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
+
+        // Test with no customizations.
+        hapticProvider =
+                new HapticFeedbackVibrationProvider(
+                        mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+        assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
+    }
+
+    private void mockVibratorPrimitiveSupport(int... supportedPrimitives) {
+        mVibrator = new FakeVibrator(mContext, supportedPrimitives);
+    }
+
+    private void mockHapticTextSupport(boolean supported) {
+        when(mResourcesMock.getBoolean(R.bool.config_enableHapticTextHandle)).thenReturn(supported);
+    }
+
+    private void mockSafeModeEnabledVibration(int... vibrationPattern) {
+        when(mResourcesMock.getIntArray(R.array.config_safeModeEnabledVibePattern))
+                .thenReturn(vibrationPattern);
+    }
+
+    private void setupCustomizationFile(String xml) throws Exception {
+        File file = new File(mContext.getCacheDir(), "test.xml");
+        file.createNewFile();
+
+        AtomicFile atomicXmlFile = new AtomicFile(file);
+        FileOutputStream fos = atomicXmlFile.startWrite();
+        fos.write(xml.getBytes());
+        atomicXmlFile.finishWrite(fos);
+
+        when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile))
+                .thenReturn(file.getAbsolutePath());
+    }
+}
diff --git a/services/tests/vibrator/utils/android/os/test/FakeVibrator.java b/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
index 56f49d4e..23398e6 100644
--- a/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
+++ b/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
@@ -18,15 +18,23 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.vibrator.IVibrator;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
 
 /** Fake implementation of {@link Vibrator} for service tests. */
 public final class FakeVibrator extends Vibrator {
+    private final VibratorInfo mVibratorInfo;
 
     public FakeVibrator(Context context) {
+        this(context, /* supportedPrimitives= */ null);
+    }
+
+    public FakeVibrator(Context context, int[] supportedPrimitives) {
         super(context);
+        mVibratorInfo = buildInfoSupportingPrimitives(supportedPrimitives);
     }
 
     @Override
@@ -40,6 +48,11 @@
     }
 
     @Override
+    public VibratorInfo getInfo() {
+        return mVibratorInfo;
+    }
+
+    @Override
     public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe, String reason,
             @NonNull VibrationAttributes attributes) {
     }
@@ -51,4 +64,16 @@
     @Override
     public void cancel(int usageFilter) {
     }
+
+    private static VibratorInfo buildInfoSupportingPrimitives(int... primitives) {
+        if (primitives == null || primitives.length == 0) {
+            return VibratorInfo.EMPTY_VIBRATOR_INFO;
+        }
+        VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        for (int primitive : primitives) {
+            builder.setSupportedPrimitive(primitive, 10);
+        }
+        return builder.build();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java b/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java
index 586bb0f..cac3262 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityThread;
+import android.app.IApplicationThread;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -91,10 +92,12 @@
         spyOn(mIWindowManager);
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
+            IApplicationThread appThread = (IApplicationThread) args[0];
             IBinder clientToken = (IBinder) args[1];
             int displayId = (int) args[3];
             DisplayContent dc = mWm.mRoot.getDisplayContent(displayId);
-            mWm.mWindowContextListenerController.registerWindowContainerListener(clientToken,
+            final WindowProcessController wpc = mAtm.getProcessController(appThread);
+            mWm.mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken,
                     dc.getImeContainer(), 1000 /* ownerUid */, TYPE_INPUT_METHOD_DIALOG,
                     null /* options */);
             return dc.getImeContainer().getConfiguration();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index e252b9e..be436bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.withSettings;
 
 import android.app.ActivityManagerInternal;
+import android.app.ActivityThread;
 import android.app.AppOpsManager;
 import android.app.IApplicationThread;
 import android.app.usage.UsageStatsManagerInternal;
@@ -321,6 +322,10 @@
                 mock(ActivityManagerService.class, withSettings().stubOnly());
         mAtmService = new TestActivityTaskManagerService(mContext, amService);
         LocalServices.addService(ActivityTaskManagerInternal.class, mAtmService.getAtmInternal());
+        // Create a fake WindowProcessController for the system process.
+        final WindowProcessController wpc =
+                addProcess("android", "system", 1485 /* pid */, 1000 /* uid */);
+        wpc.setThread(ActivityThread.currentActivityThread().getApplicationThread());
     }
 
     private void setUpWindowManagerService() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 9d597b1..6a9bb6c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -653,4 +653,23 @@
         assertEquals(mDisplayContent.getImeContainer().getParent().getSurfaceControl(),
                 mDisplayContent.computeImeParent());
     }
+
+    @Test
+    public void testIsolatedNavigation() {
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+
+        // Cannot be isolated if not embedded.
+        task.setIsolatedNav(true);
+        assertFalse(task.isIsolatedNav());
+
+        // Ensure the TaskFragment is isolated once set.
+        tf0.setIsolatedNav(true);
+        assertTrue(tf0.isIsolatedNav());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
index f6d0bf1..fa42e26 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
 
 import android.app.IWindowToken;
 import android.content.res.Configuration;
@@ -53,6 +54,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.Mockito;
 
 /**
@@ -68,11 +70,14 @@
     private static final int TEST_UID = 12345;
     private static final int ANOTHER_UID = 1000;
 
+    @Mock
+    private WindowProcessController mWpc;
     private final IBinder mClientToken = new Binder();
     private WindowContainer<?> mContainer;
 
     @Before
     public void setUp() {
+        initMocks(this);
         mController = new WindowContextListenerController();
         mContainer = createTestWindowToken(TYPE_APPLICATION_OVERLAY, mDisplayContent);
         // Make display on to verify configuration propagation.
@@ -82,20 +87,20 @@
 
     @Test
     public void testRegisterWindowContextListener() {
-        mController.registerWindowContainerListener(mClientToken, mContainer, -1,
+        mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertEquals(1, mController.mListeners.size());
 
         final IBinder clientToken = mock(IBinder.class);
-        mController.registerWindowContainerListener(clientToken, mContainer, -1,
+        mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertEquals(2, mController.mListeners.size());
 
         final WindowContainer<?> container = createTestWindowToken(TYPE_APPLICATION_OVERLAY,
                 mDefaultDisplay);
-        mController.registerWindowContainerListener(mClientToken, container, -1,
+        mController.registerWindowContainerListener(mWpc, mClientToken, container, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         // The number of listeners doesn't increase since the listener just gets updated.
@@ -121,7 +126,7 @@
         config1.densityDpi = 100;
         mContainer.onRequestedOverrideConfigurationChanged(config1);
 
-        mController.registerWindowContainerListener(clientToken, mContainer, -1,
+        mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertEquals(bounds1, clientToken.mConfiguration.windowConfiguration.getBounds());
@@ -137,7 +142,7 @@
         config2.densityDpi = 200;
         container.onRequestedOverrideConfigurationChanged(config2);
 
-        mController.registerWindowContainerListener(clientToken, container, -1,
+        mController.registerWindowContainerListener(mWpc, clientToken, container, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertEquals(bounds2, clientToken.mConfiguration.windowConfiguration.getBounds());
@@ -164,7 +169,7 @@
 
     @Test
     public void testAssertCallerCanModifyListener_CanManageAppTokens_ReturnTrue() {
-        mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID,
+        mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertTrue(mController.assertCallerCanModifyListener(mClientToken,
@@ -173,7 +178,7 @@
 
     @Test
     public void testAssertCallerCanModifyListener_SameUid_ReturnTrue() {
-        mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID,
+        mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         assertTrue(mController.assertCallerCanModifyListener(mClientToken,
@@ -182,7 +187,7 @@
 
     @Test(expected = UnsupportedOperationException.class)
     public void testAssertCallerCanModifyListener_DifferentUid_ThrowException() {
-        mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID,
+        mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         mController.assertCallerCanModifyListener(mClientToken,
@@ -198,7 +203,7 @@
                 .build();
         final DisplayArea<?> da = windowContextCreatedToken.getDisplayArea();
 
-        mController.registerWindowContainerListener(mClientToken, windowContextCreatedToken,
+        mController.registerWindowContainerListener(mWpc, mClientToken, windowContextCreatedToken,
                 TEST_UID, TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, null /* options */);
 
         assertThat(mController.getContainer(mClientToken)).isEqualTo(windowContextCreatedToken);
@@ -233,7 +238,7 @@
                 .setDisplayContent(dualDisplayContent)
                 .setFromClientToken(true)
                 .build();
-        mController.registerWindowContainerListener(mClientToken, windowContextCreatedToken,
+        mController.registerWindowContainerListener(mWpc, mClientToken, windowContextCreatedToken,
                 TEST_UID, TYPE_INPUT_METHOD_DIALOG, null /* options */);
 
         assertThat(mController.getContainer(mClientToken)).isEqualTo(windowContextCreatedToken);
@@ -258,7 +263,7 @@
         config1.densityDpi = 100;
         mContainer.onRequestedOverrideConfigurationChanged(config1);
 
-        mController.registerWindowContainerListener(mockToken, mContainer, -1,
+        mController.registerWindowContainerListener(mWpc, mockToken, mContainer, -1,
                 TYPE_APPLICATION_OVERLAY, null /* options */);
 
         verify(mockToken, never()).onConfigurationChanged(any(), anyInt());
@@ -293,7 +298,7 @@
         config1.densityDpi = 100;
         mContainer.onRequestedOverrideConfigurationChanged(config1);
 
-        mController.registerWindowContainerListener(clientToken, mContainer, -1,
+        mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1,
                 TYPE_APPLICATION_OVERLAY, options);
 
         assertThat(clientToken.mConfiguration).isEqualTo(config1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 1fa4134..495a80b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -473,7 +473,7 @@
         mWm.attachWindowContextToWindowToken(mAppThread, new Binder(), windowToken.token);
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(
-                any(), any(), anyInt(), anyInt(), any());
+                any(), any(), any(), anyInt(), anyInt(), any());
     }
 
     @Test
@@ -488,8 +488,8 @@
 
         final IBinder clientToken = new Binder();
         mWm.attachWindowContextToWindowToken(mAppThread, clientToken, windowToken.token);
-
-        verify(mWm.mWindowContextListenerController).registerWindowContainerListener(
+        final WindowProcessController wpc = mAtm.getProcessController(mAppThread);
+        verify(mWm.mWindowContextListenerController).registerWindowContainerListener(eq(wpc),
                 eq(clientToken), eq(windowToken), anyInt(), eq(TYPE_INPUT_METHOD),
                 eq(windowToken.mOptions));
     }
@@ -519,7 +519,7 @@
                 new InsetsSourceControl.Array(), new Rect(), new float[1]);
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(),
-                any(), anyInt(), anyInt(), any());
+                any(), any(), anyInt(), anyInt(), any());
     }
 
     @Test
diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
index c47d459..d9cbea9 100644
--- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
+++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
@@ -292,6 +292,19 @@
         }
     }
 
+    boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String packageName,
+            @NonNull UserHandle user) {
+        synchronized (mLock) {
+            if (doesPackageHoldExemptedPermission(packageName, user)) {
+                return true;
+            }
+            if (doesPackageHoldExemptedRole(packageName, user)) {
+                return true;
+            }
+            return false;
+        }
+    }
+
     boolean doesPackageHoldExemptedRole(@NonNull String packageName, @NonNull UserHandle user) {
         final List<String> exemptedRoles = mAppStandby.getBroadcastResponseExemptedRoles();
         synchronized (mLock) {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 43cebe8..e738d29 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2867,6 +2867,18 @@
         }
 
         @Override
+        public boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String callingPackage,
+                @UserIdInt int userId) {
+            Objects.requireNonNull(callingPackage);
+
+            getContext().enforceCallingOrSelfPermission(
+                    android.Manifest.permission.DUMP,
+                    "isPackageExemptedFromBroadcastResponseStats");
+            return mResponseStatsTracker.isPackageExemptedFromBroadcastResponseStats(
+                    callingPackage, UserHandle.of(userId));
+        }
+
+        @Override
         @Nullable
         public String getAppStandbyConstant(@NonNull String key) {
             Objects.requireNonNull(key);
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index 6722cc8..aaf0158 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.activityembedding
 
+import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.datatypes.Rect
 import android.tools.common.flicker.subject.region.RegionSubject
@@ -161,6 +162,7 @@
         }
     }
 
+    @FlakyTest(bugId = 290736037)
     /** Main activity should go from fullscreen to being a split with secondary activity. */
     @Presubmit
     @Test
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
index 49ac64c..00e75c7 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
@@ -12,15 +12,12 @@
  * 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.multishade.shared.model
+package com.android.server.wm.flicker.notification
 
-import androidx.annotation.FloatRange
+import android.tools.common.traces.component.ComponentNameMatcher
 
-/** Models the current state of a shade. */
-data class ShadeModel(
-    val id: ShadeId,
-    @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
-)
+object Consts {
+    val IMAGE_WALLPAPER = ComponentNameMatcher("", "com.android.systemui.wallpapers.ImageWallpaper")
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
index 6f5daeb..6819a38 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
@@ -17,8 +17,10 @@
 package com.android.server.wm.flicker.notification
 
 import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
 import android.platform.test.rule.SettingOverrideRule
 import android.provider.Settings
+import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
@@ -107,6 +109,21 @@
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+        flicker.assertWm {
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry(
+                listOf(
+                    ComponentNameMatcher.SPLASH_SCREEN,
+                    ComponentNameMatcher.SNAPSHOT,
+                    ComponentNameMatcher.SECONDARY_HOME_HANDLE,
+                    Consts.IMAGE_WALLPAPER
+                )
+            )
+        }
+    }
+
     companion object {
         /**
          * Creates the test configurations.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
index 483caa7..bf0f4ab 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
@@ -139,6 +139,21 @@
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+        flicker.assertWm {
+            this.visibleWindowsShownMoreThanOneConsecutiveEntry(
+                listOf(
+                    ComponentNameMatcher.SPLASH_SCREEN,
+                    ComponentNameMatcher.SNAPSHOT,
+                    ComponentNameMatcher.SECONDARY_HOME_HANDLE,
+                    Consts.IMAGE_WALLPAPER
+                )
+            )
+        }
+    }
+
     companion object {
         /**
          * Creates the test configurations.
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
index 7cf69b7..31ea832 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
@@ -16,6 +16,10 @@
 
 package com.android.test.silkfx.hdr
 
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Canvas
@@ -65,15 +69,9 @@
         findViewById<RadioGroup>(R.id.output_mode)!!.also {
             it.check(outputMode)
             it.setOnCheckedChangeListener { _, checkedId ->
-                val previousMode = outputMode
                 outputMode = checkedId
-                if (previousMode == R.id.output_sdr && checkedId == R.id.output_hdr) {
-                    animateToHdr()
-                } else if (previousMode == R.id.output_hdr && checkedId == R.id.output_sdr) {
-                    animateToSdr()
-                } else {
-                    updateDisplay()
-                }
+                // Intentionally don't do anything fancy so that mode A/B comparisons are easy
+                updateDisplay()
             }
         }
 
@@ -103,10 +101,41 @@
 
         imageView.apply {
             isClickable = true
+            // Example of animating between SDR and HDR using gainmap params; animates HDR->SDR->HDR
+            // with a brief pause on SDR. The key thing here is that the gainmap's
+            // minDisplayRatioForHdrTransition is animated between its original value (for full HDR)
+            // and displayRatioForFullHdr (for full SDR). The view must also be invalidated during
+            // the animation for the updates to take effect.
             setOnClickListener {
-                animate().alpha(.5f).withEndAction {
-                    animate().alpha(1f).start()
-                }.start()
+                if (gainmap != null && (outputMode == R.id.output_hdr ||
+                                        outputMode == R.id.output_hdr_test)) {
+                    val animationLengthMs: Long = 500
+                    val updateListener = object : AnimatorUpdateListener {
+                        override fun onAnimationUpdate(animation: ValueAnimator) {
+                            imageView.invalidate()
+                        }
+                    }
+                    val hdrToSdr = ObjectAnimator.ofFloat(
+                      gainmap, "minDisplayRatioForHdrTransition",
+                      gainmap!!.minDisplayRatioForHdrTransition,
+                      gainmap!!.displayRatioForFullHdr).apply {
+                        duration = animationLengthMs
+                        addUpdateListener(updateListener)
+                    }
+                    val sdrToHdr = ObjectAnimator.ofFloat(
+                      gainmap, "minDisplayRatioForHdrTransition",
+                      gainmap!!.displayRatioForFullHdr,
+                      gainmap!!.minDisplayRatioForHdrTransition).apply {
+                        duration = animationLengthMs
+                        addUpdateListener(updateListener)
+                    }
+
+                    AnimatorSet().apply {
+                        play(hdrToSdr)
+                        play(sdrToHdr).after(animationLengthMs)
+                        start()
+                    }
+                }
             }
         }
     }
@@ -164,20 +193,6 @@
         updateDisplay()
     }
 
-    private fun animateToHdr() {
-        if (bitmap == null || gainmap == null) return
-
-        // TODO: Trigger an animation
-        updateDisplay()
-    }
-
-    private fun animateToSdr() {
-        if (bitmap == null) return
-
-        // TODO: Trigger an animation
-        updateDisplay()
-    }
-
     private fun updateDisplay() {
         if (bitmap == null) return
 
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index d2ea599..b5c290e 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -186,7 +186,20 @@
     // These are created as weak symbols, and are only generated from default
     // configuration
     // strings and plurals.
-    PseudolocaleGenerator pseudolocale_generator;
+    std::string grammatical_gender_values;
+    std::string grammatical_gender_ratio;
+    if (options.pseudo_localize_gender_values) {
+      grammatical_gender_values = options.pseudo_localize_gender_values.value();
+    } else {
+      grammatical_gender_values = "f,m,n";
+    }
+    if (options.pseudo_localize_gender_ratio) {
+      grammatical_gender_ratio = options.pseudo_localize_gender_ratio.value();
+    } else {
+      grammatical_gender_ratio = "1.0";
+    }
+    PseudolocaleGenerator pseudolocale_generator(grammatical_gender_values,
+                                                 grammatical_gender_ratio);
     if (!pseudolocale_generator.Consume(context, &table)) {
       return false;
     }
diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h
index 14a730a..22890fc 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -35,6 +35,8 @@
   std::optional<std::string> res_dir;
   std::optional<std::string> res_zip;
   std::optional<std::string> generate_text_symbols_path;
+  std::optional<std::string> pseudo_localize_gender_values;
+  std::optional<std::string> pseudo_localize_gender_ratio;
   std::optional<Visibility::Level> visibility;
   bool pseudolocalize = false;
   bool no_png_crunch = false;
@@ -76,6 +78,15 @@
     AddOptionalFlag("--source-path",
                       "Sets the compiled resource file source file path to the given string.",
                       &options_.source_path);
+    AddOptionalFlag("--pseudo-localize-gender-values",
+                    "Sets the gender values to pick up for generating grammatical gender strings, "
+                    "gender values should be f, m, or n, which are shortcuts for feminine, "
+                    "masculine and neuter, and split with comma.",
+                    &options_.pseudo_localize_gender_values);
+    AddOptionalFlag("--pseudo-localize-gender-ratio",
+                    "Sets the ratio of resources to generate grammatical gender strings for. The "
+                    "ratio has to be a float number between 0 and 1.",
+                    &options_.pseudo_localize_gender_ratio);
   }
 
   int Action(const std::vector<std::string>& args) override;
diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp
index 3464a76..8880089 100644
--- a/tools/aapt2/cmd/Compile_test.cpp
+++ b/tools/aapt2/cmd/Compile_test.cpp
@@ -236,9 +236,24 @@
   // The first string (000) is translatable, the second is not
   // ar-XB uses "\u200F\u202E...\u202C\u200F"
   std::vector<std::string> expected_translatable = {
-      "000", "111", // default locale
-      "[000 one]", // en-XA
-      "\xE2\x80\x8F\xE2\x80\xAE" "000" "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB
+      "(F)[000 one]",  // en-XA-feminine
+      "(F)\xE2\x80\x8F\xE2\x80\xAE"
+      "000"
+      "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-feminine
+      "(M)[000 one]",              // en-XA-masculine
+      "(M)\xE2\x80\x8F\xE2\x80\xAE"
+      "000"
+      "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-masculine
+      "(N)[000 one]",              // en-XA-neuter
+      "(N)\xE2\x80\x8F\xE2\x80\xAE"
+      "000"
+      "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-neuter
+      "000",                       // default locale
+      "111",                       // default locale
+      "[000 one]",                 // en-XA
+      "\xE2\x80\x8F\xE2\x80\xAE"
+      "000"
+      "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB
   };
   AssertTranslations(this, "foo", expected_translatable);
   AssertTranslations(this, "foo_donottranslate", expected_translatable);
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp
index 09a8560..8143052 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp
@@ -16,11 +16,15 @@
 
 #include "compile/PseudolocaleGenerator.h"
 
+#include <stdint.h>
+
 #include <algorithm>
+#include <random>
 
 #include "ResourceTable.h"
 #include "ResourceValues.h"
 #include "ValueVisitor.h"
+#include "androidfw/ResourceTypes.h"
 #include "androidfw/Util.h"
 #include "compile/Pseudolocalizer.h"
 #include "util/Util.h"
@@ -293,8 +297,85 @@
   Pseudolocalizer localizer_;
 };
 
+class GrammaticalGenderVisitor : public ValueVisitor {
+ public:
+  std::unique_ptr<Value> value;
+  std::unique_ptr<Item> item;
+
+  GrammaticalGenderVisitor(android::StringPool* pool, uint8_t grammaticalInflection)
+      : pool_(pool), grammaticalInflection_(grammaticalInflection) {
+  }
+
+  void Visit(Plural* plural) override {
+    CloningValueTransformer cloner(pool_);
+    std::unique_ptr<Plural> grammatical_gendered = util::make_unique<Plural>();
+    for (size_t i = 0; i < plural->values.size(); i++) {
+      if (plural->values[i]) {
+        GrammaticalGenderVisitor sub_visitor(pool_, grammaticalInflection_);
+        plural->values[i]->Accept(&sub_visitor);
+        if (sub_visitor.item) {
+          grammatical_gendered->values[i] = std::move(sub_visitor.item);
+        } else {
+          grammatical_gendered->values[i] = plural->values[i]->Transform(cloner);
+        }
+      }
+    }
+    grammatical_gendered->SetSource(plural->GetSource());
+    grammatical_gendered->SetWeak(true);
+    value = std::move(grammatical_gendered);
+  }
+
+  std::string AddGrammaticalGenderPrefix(const std::string_view& original_string) {
+    std::string result;
+    switch (grammaticalInflection_) {
+      case android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE:
+        result = std::string("(M)") + std::string(original_string);
+        break;
+      case android::ResTable_config::GRAMMATICAL_GENDER_FEMININE:
+        result = std::string("(F)") + std::string(original_string);
+        break;
+      case android::ResTable_config::GRAMMATICAL_GENDER_NEUTER:
+        result = std::string("(N)") + std::string(original_string);
+        break;
+      default:
+        result = std::string(original_string);
+        break;
+    }
+    return result;
+  }
+
+  void Visit(String* string) override {
+    std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(*string->value));
+    std::unique_ptr<String> grammatical_gendered =
+        util::make_unique<String>(pool_->MakeRef(prefixed_string));
+    grammatical_gendered->SetSource(string->GetSource());
+    grammatical_gendered->SetWeak(true);
+    item = std::move(grammatical_gendered);
+  }
+
+  void Visit(StyledString* string) override {
+    std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(string->value->value));
+    android::StyleString new_string;
+    new_string.str = std::move(prefixed_string);
+    for (const android::StringPool::Span& span : string->value->spans) {
+      new_string.spans.emplace_back(android::Span{*span.name, span.first_char, span.last_char});
+    }
+    std::unique_ptr<StyledString> grammatical_gendered =
+        util::make_unique<StyledString>(pool_->MakeRef(new_string));
+    grammatical_gendered->SetSource(string->GetSource());
+    grammatical_gendered->SetWeak(true);
+    item = std::move(grammatical_gendered);
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(GrammaticalGenderVisitor);
+  android::StringPool* pool_;
+  uint8_t grammaticalInflection_;
+};
+
 ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base,
-                                              Pseudolocalizer::Method m) {
+                                              Pseudolocalizer::Method m,
+                                              uint8_t grammaticalInflection) {
   ConfigDescription modified = base;
   switch (m) {
     case Pseudolocalizer::Method::kAccent:
@@ -313,12 +394,64 @@
     default:
       break;
   }
+  modified.grammaticalInflection = grammaticalInflection;
   return modified;
 }
 
+void GrammaticalGender(ResourceConfigValue* original_value,
+                       ResourceConfigValue* localized_config_value, android::StringPool* pool,
+                       ResourceEntry* entry, const Pseudolocalizer::Method method,
+                       uint8_t grammaticalInflection) {
+  GrammaticalGenderVisitor visitor(pool, grammaticalInflection);
+  localized_config_value->value->Accept(&visitor);
+
+  std::unique_ptr<Value> grammatical_gendered_value;
+  if (visitor.value) {
+    grammatical_gendered_value = std::move(visitor.value);
+  } else if (visitor.item) {
+    grammatical_gendered_value = std::move(visitor.item);
+  }
+  if (!grammatical_gendered_value) {
+    return;
+  }
+
+  ConfigDescription config =
+      ModifyConfigForPseudoLocale(original_value->config, method, grammaticalInflection);
+
+  ResourceConfigValue* grammatical_gendered_config_value =
+      entry->FindOrCreateValue(config, original_value->product);
+  if (!grammatical_gendered_config_value->value) {
+    // Only use auto-generated pseudo-localization if none is defined.
+    grammatical_gendered_config_value->value = std::move(grammatical_gendered_value);
+  }
+}
+
+const uint32_t MASK_MASCULINE = 1;  // Bit mask for masculine
+const uint32_t MASK_FEMININE = 2;   // Bit mask for feminine
+const uint32_t MASK_NEUTER = 4;     // Bit mask for neuter
+
+void GrammaticalGenderIfNeeded(ResourceConfigValue* original_value, ResourceConfigValue* new_value,
+                               android::StringPool* pool, ResourceEntry* entry,
+                               const Pseudolocalizer::Method method, uint32_t gender_state) {
+  if (gender_state & MASK_FEMININE) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_FEMININE);
+  }
+
+  if (gender_state & MASK_MASCULINE) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE);
+  }
+
+  if (gender_state & MASK_NEUTER) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_NEUTER);
+  }
+}
+
 void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method,
                             ResourceConfigValue* original_value, android::StringPool* pool,
-                            ResourceEntry* entry) {
+                            ResourceEntry* entry, uint32_t gender_state, bool gender_flag) {
   Visitor visitor(pool, method);
   original_value->value->Accept(&visitor);
 
@@ -333,8 +466,8 @@
     return;
   }
 
-  ConfigDescription config_with_accent =
-      ModifyConfigForPseudoLocale(original_value->config, method);
+  ConfigDescription config_with_accent = ModifyConfigForPseudoLocale(
+      original_value->config, method, android::ResTable_config::GRAMMATICAL_GENDER_ANY);
 
   ResourceConfigValue* new_config_value =
       entry->FindOrCreateValue(config_with_accent, original_value->product);
@@ -342,6 +475,9 @@
     // Only use auto-generated pseudo-localization if none is defined.
     new_config_value->value = std::move(localized_value);
   }
+  if (gender_flag) {
+    GrammaticalGenderIfNeeded(original_value, new_config_value, pool, entry, method, gender_state);
+  }
 }
 
 // A value is pseudolocalizable if it does not define a locale (or is the default locale) and is
@@ -356,16 +492,71 @@
 
 }  // namespace
 
+bool ParseGenderValuesAndSaveState(const std::string& grammatical_gender_values,
+                                   uint32_t* gender_state, android::IDiagnostics* diag) {
+  std::vector<std::string> values = util::SplitAndLowercase(grammatical_gender_values, ',');
+  for (size_t i = 0; i < values.size(); i++) {
+    if (values[i].length() != 0) {
+      if (values[i] == "f") {
+        *gender_state |= MASK_FEMININE;
+      } else if (values[i] == "m") {
+        *gender_state |= MASK_MASCULINE;
+      } else if (values[i] == "n") {
+        *gender_state |= MASK_NEUTER;
+      } else {
+        diag->Error(android::DiagMessage() << "Invalid grammatical gender value: " << values[i]);
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+bool ParseGenderRatio(const std::string& grammatical_gender_ratio, float* gender_ratio,
+                      android::IDiagnostics* diag) {
+  const char* input = grammatical_gender_ratio.c_str();
+  char* endPtr;
+  errno = 0;
+  *gender_ratio = strtof(input, &endPtr);
+  if (endPtr == input || *endPtr != '\0' || errno == ERANGE || *gender_ratio < 0 ||
+      *gender_ratio > 1) {
+    diag->Error(android::DiagMessage()
+                << "Invalid grammatical gender ratio: " << grammatical_gender_ratio
+                << ", must be a real number between 0 and 1");
+    return false;
+  }
+  return true;
+}
+
 bool PseudolocaleGenerator::Consume(IAaptContext* context, ResourceTable* table) {
+  uint32_t gender_state = 0;
+  if (!ParseGenderValuesAndSaveState(grammatical_gender_values_, &gender_state,
+                                     context->GetDiagnostics())) {
+    return false;
+  }
+
+  float gender_ratio = 0;
+  if (!ParseGenderRatio(grammatical_gender_ratio_, &gender_ratio, context->GetDiagnostics())) {
+    return false;
+  }
+
+  std::random_device rd;
+  std::mt19937 gen(rd());
+  std::uniform_real_distribution<> distrib(0.0, 1.0);
+
   for (auto& package : table->packages) {
     for (auto& type : package->types) {
       for (auto& entry : type->entries) {
+        bool gender_flag = false;
+        if (distrib(gen) < gender_ratio) {
+          gender_flag = true;
+        }
         std::vector<ResourceConfigValue*> values = entry->FindValuesIf(IsPseudolocalizable);
         for (ResourceConfigValue* value : values) {
           PseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value, &table->string_pool,
-                                 entry.get());
+                                 entry.get(), gender_state, gender_flag);
           PseudolocalizeIfNeeded(Pseudolocalizer::Method::kBidi, value, &table->string_pool,
-                                 entry.get());
+                                 entry.get(), gender_state, gender_flag);
         }
       }
     }
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.h b/tools/aapt2/compile/PseudolocaleGenerator.h
index 44e6e3e..ce92008 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.h
+++ b/tools/aapt2/compile/PseudolocaleGenerator.h
@@ -27,8 +27,19 @@
                                                          Pseudolocalizer::Method method,
                                                          android::StringPool* pool);
 
-struct PseudolocaleGenerator : public IResourceTableConsumer {
-  bool Consume(IAaptContext* context, ResourceTable* table) override;
+class PseudolocaleGenerator : public IResourceTableConsumer {
+ public:
+  explicit PseudolocaleGenerator(std::string grammatical_gender_values,
+                                 std::string grammatical_gender_ratio)
+      : grammatical_gender_values_(std::move(grammatical_gender_values)),
+        grammatical_gender_ratio_(std::move(grammatical_gender_ratio)) {
+  }
+
+  bool Consume(IAaptContext* context, ResourceTable* table);
+
+ private:
+  std::string grammatical_gender_values_;
+  std::string grammatical_gender_ratio_;
 };
 
 }  // namespace aapt
diff --git a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
index 2f90cbf..1477ebf 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
@@ -197,7 +197,7 @@
   val->SetTranslatable(false);
 
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   // Normal pseudolocalization should take place.
@@ -249,7 +249,7 @@
   expected->values = {util::make_unique<String>(table->string_pool.MakeRef("[žéŕö one]")),
                       util::make_unique<String>(table->string_pool.MakeRef("[öñé one]"))};
 
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   const auto* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo",
@@ -287,7 +287,7 @@
                                    context->GetDiagnostics()));
   }
 
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   StyledString* new_styled_string = test::GetValueForConfig<StyledString>(
@@ -305,4 +305,213 @@
   EXPECT_NE(std::string::npos, new_string->value->find("world"));
 }
 
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForString) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  String* locale = test::GetValueForConfig<String>(table.get(), "android:string/foo",
+                                                   test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale);
+
+  // Grammatical gendered string
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* feminine =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine);
+  EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* masculine =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_masculine);
+  ASSERT_NE(nullptr, masculine);
+  EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* neuter =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter);
+  EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value);
+}
+
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForPlural) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+  std::unique_ptr<Plural> plural = util::make_unique<Plural>();
+  plural->values = {util::make_unique<String>(table->string_pool.MakeRef("zero")),
+                    util::make_unique<String>(table->string_pool.MakeRef("one"))};
+  ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("com.pkg:plurals/foo"))
+                                     .SetValue(std::move(plural))
+                                     .Build(),
+                                 context->GetDiagnostics()));
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  Plural* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo",
+                                                   test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, actual);
+
+  // Grammatical gendered Plural
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_feminine =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_feminine);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* feminine = ValueCast<String>(actual_feminine->values[i].get());
+      EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value);
+    }
+  }
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_masculine =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_masculine);
+  ASSERT_NE(nullptr, actual_masculine);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* masculine = ValueCast<String>(actual_masculine->values[i].get());
+      EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value);
+    }
+  }
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_neuter =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_neuter);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* neuter = ValueCast<String>(actual_neuter->values[i].get());
+      EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value);
+    }
+  }
+}
+
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForStyledString) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+  android::StyleString original_style;
+  original_style.str = "Hello world!";
+  original_style.spans = {android::Span{"i", 1, 10}};
+
+  std::unique_ptr<StyledString> original =
+      util::make_unique<StyledString>(table->string_pool.MakeRef(original_style));
+  ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("android:string/foo"))
+                                     .SetValue(std::move(original))
+                                     .Build(),
+                                 context->GetDiagnostics()));
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  StyledString* locale = test::GetValueForConfig<StyledString>(table.get(), "android:string/foo",
+                                                               test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale);
+  EXPECT_EQ(1, locale->value->spans.size());
+  EXPECT_EQ(std::string("i"), *locale->value->spans[0].name);
+
+  // Grammatical gendered StyledString
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* feminine =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine);
+  EXPECT_EQ(1, feminine->value->spans.size());
+  EXPECT_EQ(std::string("i"), *feminine->value->spans[0].name);
+  EXPECT_EQ(std::string("(F)") + locale->value->value, feminine->value->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* masculine =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_masculine);
+  ASSERT_NE(nullptr, masculine);
+  EXPECT_EQ(1, masculine->value->spans.size());
+  EXPECT_EQ(std::string("i"), *masculine->value->spans[0].name);
+  EXPECT_EQ(std::string("(M)") + locale->value->value, masculine->value->value);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* neuter =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter);
+  EXPECT_EQ(1, neuter->value->spans.size());
+  EXPECT_EQ(std::string("i"), *neuter->value->spans[0].name);
+  EXPECT_EQ(std::string("(N)") + locale->value->value, neuter->value->value);
+}
+
+TEST(PseudolocaleGeneratorTest, GrammaticalGenderForCertainValues) {
+  // single gender value
+  std::unique_ptr<ResourceTable> table_0 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_0 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_0(std::string("f"), std::string("1.0"));
+  ASSERT_TRUE(generator_0.Consume(context_0.get(), table_0.get()));
+
+  String* locale_0 = test::GetValueForConfig<String>(table_0.get(), "android:string/foo",
+                                                     test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale_0);
+
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* feminine_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine_0);
+  EXPECT_EQ(std::string("(F)") + *locale_0->value, *feminine_0->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* masculine_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_masculine);
+  EXPECT_EQ(nullptr, masculine_0);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* neuter_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_neuter);
+  EXPECT_EQ(nullptr, neuter_0);
+
+  // multiple gender values
+  std::unique_ptr<ResourceTable> table_1 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_1 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_1(std::string("f,n"), std::string("1.0"));
+  ASSERT_TRUE(generator_1.Consume(context_1.get(), table_1.get()));
+
+  String* locale_1 = test::GetValueForConfig<String>(table_1.get(), "android:string/foo",
+                                                     test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale_1);
+
+  String* feminine_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine_1);
+  EXPECT_EQ(std::string("(F)") + *locale_1->value, *feminine_1->value);
+
+  String* masculine_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_masculine);
+  EXPECT_EQ(nullptr, masculine_1);
+
+  String* neuter_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter_1);
+  EXPECT_EQ(std::string("(N)") + *locale_1->value, *neuter_1->value);
+
+  // invalid gender value
+  std::unique_ptr<ResourceTable> table_2 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_2 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_2(std::string("invald,"), std::string("1.0"));
+  ASSERT_FALSE(generator_2.Consume(context_2.get(), table_2.get()));
+}
+
 }  // namespace aapt