Merge changes Iff50fbf8,Ibfd590be,Ibc4484bb into main

* changes:
  Add logging to LogBuffer
  Refactor VisualInterruptionDecisionProviderImpl
  Improve tests and add missing test cases
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 44f3d70..52200bf 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -180,6 +180,18 @@
     srcs: ["core/java/android/nfc/*.aconfig"],
 }
 
+cc_aconfig_library {
+    name: "android_nfc_flags_aconfig_c_lib",
+    vendor_available: true,
+    aconfig_declarations: "android.nfc.flags-aconfig",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.nfcservices",
+        "nfc_nci.st21nfc.default",
+    ],
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 java_aconfig_library {
     name: "android.nfc.flags-aconfig-java",
     aconfig_declarations: "android.nfc.flags-aconfig",
diff --git a/core/api/current.txt b/core/api/current.txt
index 3f4a34b..55933d43 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -14699,31 +14699,31 @@
   }
 
   @FlaggedApi("android.database.sqlite.sqlite_apis_35") public final class SQLiteRawStatement implements java.io.Closeable {
-    method public void bindBlob(int, @NonNull byte[]) throws android.database.sqlite.SQLiteException;
-    method public void bindBlob(int, @NonNull byte[], int, int) throws android.database.sqlite.SQLiteException;
-    method public void bindDouble(int, double) throws android.database.sqlite.SQLiteException;
-    method public void bindInt(int, int) throws android.database.sqlite.SQLiteException;
-    method public void bindLong(int, long) throws android.database.sqlite.SQLiteException;
-    method public void bindNull(int) throws android.database.sqlite.SQLiteException;
-    method public void bindText(int, @NonNull String) throws android.database.sqlite.SQLiteException;
+    method public void bindBlob(int, @NonNull byte[]);
+    method public void bindBlob(int, @NonNull byte[], int, int);
+    method public void bindDouble(int, double);
+    method public void bindInt(int, int);
+    method public void bindLong(int, long);
+    method public void bindNull(int);
+    method public void bindText(int, @NonNull String);
     method public void clearBindings();
     method public void close();
-    method @Nullable public byte[] getColumnBlob(int) throws android.database.sqlite.SQLiteException;
-    method public double getColumnDouble(int) throws android.database.sqlite.SQLiteException;
-    method public int getColumnInt(int) throws android.database.sqlite.SQLiteException;
-    method public int getColumnLength(int) throws android.database.sqlite.SQLiteException;
-    method public long getColumnLong(int) throws android.database.sqlite.SQLiteException;
-    method @NonNull public String getColumnName(int) throws android.database.sqlite.SQLiteException;
-    method @NonNull public String getColumnText(int) throws android.database.sqlite.SQLiteException;
-    method public int getColumnType(int) throws android.database.sqlite.SQLiteException;
+    method @Nullable public byte[] getColumnBlob(int);
+    method public double getColumnDouble(int);
+    method public int getColumnInt(int);
+    method public int getColumnLength(int);
+    method public long getColumnLong(int);
+    method @NonNull public String getColumnName(int);
+    method @NonNull public String getColumnText(int);
+    method public int getColumnType(int);
     method public int getParameterCount();
     method public int getParameterIndex(@NonNull String);
     method @Nullable public String getParameterName(int);
     method public int getResultColumnCount();
     method public boolean isOpen();
-    method public int readColumnBlob(int, @NonNull byte[], int, int, int) throws android.database.sqlite.SQLiteException;
+    method public int readColumnBlob(int, @NonNull byte[], int, int, int);
     method public void reset();
-    method public boolean step() throws android.database.sqlite.SQLiteException;
+    method public boolean step();
     field public static final int SQLITE_DATA_TYPE_BLOB = 4; // 0x4
     field public static final int SQLITE_DATA_TYPE_FLOAT = 2; // 0x2
     field public static final int SQLITE_DATA_TYPE_INTEGER = 1; // 0x1
@@ -28698,14 +28698,17 @@
   }
 
   public final class NfcAdapter {
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean allowTransaction();
     method public void disableForegroundDispatch(android.app.Activity);
     method public void disableReaderMode(android.app.Activity);
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean disallowTransaction();
     method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]);
     method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
     method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
     method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo();
     method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler);
     method public boolean isEnabled();
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeSupported();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionEnabled();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionSupported();
     method public boolean isSecureNfcEnabled();
@@ -28813,6 +28816,7 @@
     method public boolean removeAidsForService(android.content.ComponentName, String);
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String);
     method public boolean setPreferredService(android.app.Activity, android.content.ComponentName);
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setServiceObserveModeDefault(@NonNull android.content.ComponentName, boolean);
     method public boolean supportsAidPrefixRegistration();
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName);
     method public boolean unsetPreferredService(android.app.Activity);
@@ -28832,9 +28836,20 @@
     method public final android.os.IBinder onBind(android.content.Intent);
     method public abstract void onDeactivated(int);
     method public abstract byte[] processCommandApdu(byte[], android.os.Bundle);
+    method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void processPollingFrames(@NonNull java.util.List<android.os.Bundle>);
     method public final void sendResponseApdu(byte[]);
     field public static final int DEACTIVATION_DESELECTED = 1; // 0x1
     field public static final int DEACTIVATION_LINK_LOSS = 0; // 0x0
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_A = 65; // 0x0041 'A'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_B = 66; // 0x0042 'B'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_F = 70; // 0x0046 'F'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_OFF = 88; // 0x0058 'X'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_ON = 79; // 0x004f 'O'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_UNKNOWN = 85; // 0x0055 'U'
     field public static final String SERVICE_INTERFACE = "android.nfc.cardemulation.action.HOST_APDU_SERVICE";
     field public static final String SERVICE_META_DATA = "android.nfc.cardemulation.host_apdu_service";
   }
@@ -37273,6 +37288,7 @@
   }
 
   public static final class Telephony.Carriers implements android.provider.BaseColumns {
+    field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String ALWAYS_ON = "always_on";
     field public static final String APN = "apn";
     field public static final String AUTH_TYPE = "authtype";
     field @Deprecated public static final String BEARER = "bearer";
@@ -37286,6 +37302,8 @@
     field public static final String MMSPORT = "mmsport";
     field public static final String MMSPROXY = "mmsproxy";
     field @Deprecated public static final String MNC = "mnc";
+    field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V4 = "mtu_v4";
+    field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V6 = "mtu_v6";
     field @Deprecated public static final String MVNO_MATCH_DATA = "mvno_match_data";
     field @Deprecated public static final String MVNO_TYPE = "mvno_type";
     field public static final String NAME = "name";
@@ -37301,6 +37319,8 @@
     field public static final String SUBSCRIPTION_ID = "sub_id";
     field public static final String TYPE = "type";
     field public static final String USER = "user";
+    field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_EDITABLE = "user_editable";
+    field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_VISIBLE = "user_visible";
   }
 
   public static final class Telephony.Mms implements android.provider.Telephony.BaseMmsColumns {
@@ -45817,6 +45837,7 @@
     method public int getProxyPort();
     method public int getRoamingProtocol();
     method public String getUser();
+    method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public boolean isAlwaysOn();
     method public boolean isEnabled();
     method public boolean isPersistent();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -45856,6 +45877,7 @@
   public static class ApnSetting.Builder {
     ctor public ApnSetting.Builder();
     method public android.telephony.data.ApnSetting build();
+    method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") @NonNull public android.telephony.data.ApnSetting.Builder setAlwaysOn(boolean);
     method @NonNull public android.telephony.data.ApnSetting.Builder setApnName(@Nullable String);
     method @NonNull public android.telephony.data.ApnSetting.Builder setApnTypeBitmask(int);
     method @NonNull public android.telephony.data.ApnSetting.Builder setAuthType(int);
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 1308b1f..449249e 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -1,40 +1,4 @@
 // Baseline format: 1.0
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindBlob(int, byte[]):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindBlob(int, byte[], int, int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindDouble(int, double):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindInt(int, int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindLong(int, long):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindNull(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#bindText(int, String):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnBlob(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnDouble(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnInt(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnLength(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnLong(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnName(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnText(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#getColumnType(int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#readColumnBlob(int, byte[], int, int, int):
-    Methods must not throw unchecked exceptions
-BannedThrow: android.database.sqlite.SQLiteRawStatement#step():
-    Methods must not throw unchecked exceptions
-
-
 BroadcastBehavior: android.app.AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED:
     Field 'ACTION_NEXT_ALARM_CLOCK_CHANGED' is missing @BroadcastBehavior
 BroadcastBehavior: android.app.AlarmManager#ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED:
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index de07fdde6..d96925d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11234,15 +11234,11 @@
     field public static final String MAX_CONNECTIONS = "max_conns";
     field public static final String MODEM_PERSIST = "modem_cognitive";
     field @Deprecated public static final String MTU = "mtu";
-    field public static final String MTU_V4 = "mtu_v4";
-    field public static final String MTU_V6 = "mtu_v6";
     field public static final int NO_APN_SET_ID = 0; // 0x0
     field public static final String TIME_LIMIT_FOR_MAX_CONNECTIONS = "max_conns_time";
     field public static final int UNEDITED = 0; // 0x0
     field public static final int USER_DELETED = 2; // 0x2
-    field public static final String USER_EDITABLE = "user_editable";
     field public static final int USER_EDITED = 1; // 0x1
-    field public static final String USER_VISIBLE = "user_visible";
     field public static final String WAIT_TIME_RETRY = "wait_time";
   }
 
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index b0332c3..ffb79b3 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1642,6 +1642,7 @@
      *
      * <p>
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public Policy getNotificationPolicy() {
         INotificationManager service = getService();
         try {
@@ -1660,6 +1661,7 @@
      *
      * @param policy The new desired policy.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
         checkRequired("policy", policy);
         INotificationManager service = getService();
@@ -2608,6 +2610,7 @@
      * Only available if policy access is granted to this package. See
      * {@link #isNotificationPolicyAccessGranted}.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
         final INotificationManager service = getService();
         try {
diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java
index a4d5327..cc12949 100644
--- a/core/java/android/content/pm/LauncherActivityInfo.java
+++ b/core/java/android/content/pm/LauncherActivityInfo.java
@@ -22,11 +22,18 @@
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
+import android.icu.text.UnicodeSet;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.text.TextUtils;
 import android.util.DisplayMetrics;
 
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
 /**
  * A representation of an activity that can belong to this user or a managed
  * profile associated with this user. It can be used to query the label, icon
@@ -36,6 +43,10 @@
     private final PackageManager mPm;
     private final LauncherActivityInfoInternal mInternal;
 
+    private static final UnicodeSet TRIMMABLE_CHARACTERS =
+            new UnicodeSet("[[:White_Space:][:Default_Ignorable_Code_Point:][:gc=Cc:]]",
+                    /* ignoreWhitespace= */ false).freeze();
+
     /**
      * Create a launchable activity object for a given ResolveInfo and user.
      *
@@ -72,13 +83,28 @@
     }
 
     /**
-     * Retrieves the label for the activity.
+     * Retrieves the label for the activity. The returned label can be different
+     * from {@link ActivityInfo#loadLabel(PackageManager)} or
+     * {@link PackageItemInfo#loadLabel(PackageManager)}. The returned result is trimmed.
+     * If the activity's label is empty, use the application's label instead.
+     * If the application's label is still empty, use the package name instead.
      *
-     * @return The label for the activity.
+     * @return The label for the activity. If the activity's label is empty,
+     *         return the application's label instead. If the application's label
+     *         is still empty, return the package name instead.
      */
     public CharSequence getLabel() {
+        CharSequence label = trim(getActivityInfo().loadLabel(mPm));
+        // If the trimmed label is empty, use application's label instead
+        if (TextUtils.isEmpty(label)) {
+            label = trim(getApplicationInfo().loadLabel(mPm));
+            // If the trimmed label is still empty, use package name instead
+            if (TextUtils.isEmpty(label)) {
+                label = getComponentName().getPackageName();
+            }
+        }
         // TODO: Go through LauncherAppsService
-        return getActivityInfo().loadLabel(mPm);
+        return label;
     }
 
     /**
@@ -180,4 +206,149 @@
 
         return mPm.getUserBadgedIcon(originalIcon, mInternal.getUser());
     }
+
+    /**
+     * If the {@code ch} is trimmable, return {@code true}. Otherwise, return
+     * {@code false}. If the count of the code points of {@code ch} doesn't
+     * equal 1, return {@code false}.
+     * <p>
+     * There are two types of the trimmable characters.
+     * 1. The character is one of the Default_Ignorable_Code_Point in
+     * <a href="
+     * https://www.unicode.org/Public/UCD/latest/ucd/DerivedCoreProperties.txt">
+     * DerivedCoreProperties.txt</a>, the White_Space in <a href=
+     * "https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt">PropList.txt
+     * </a> or category Cc.
+     * <p>
+     * 2. The character is not supported in the current system font.
+     * {@link android.graphics.Paint#hasGlyph(String)}
+     * <p>
+     *
+     */
+    private static boolean isTrimmable(@NonNull Paint paint, @NonNull CharSequence ch) {
+        Objects.requireNonNull(paint);
+        Objects.requireNonNull(ch);
+
+        // if ch is empty or it is not a character (i,e, the count of code
+        // point doesn't equal one), return false
+        if (TextUtils.isEmpty(ch)
+                || Character.codePointCount(ch, /* beginIndex= */ 0, ch.length()) != 1) {
+            return false;
+        }
+
+        // Return true for the cases as below:
+        // 1. The character is in the TRIMMABLE_CHARACTERS set
+        // 2. The character is not supported in the system font
+        return TRIMMABLE_CHARACTERS.contains(ch) || !paint.hasGlyph(ch.toString());
+    }
+
+    /**
+     * If the {@code sequence} has some leading trimmable characters, creates a new copy
+     * and removes the trimmable characters from the copy. Otherwise the given
+     * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
+     * to determine whether the character is trimmable or not.
+     *
+     * @return the trimmed string or the original string that has no
+     *         leading trimmable characters.
+     * @see    #isTrimmable(Paint, CharSequence)
+     * @see    #trim(CharSequence)
+     * @see    #trimEnd(CharSequence)
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    @NonNull
+    public static CharSequence trimStart(@NonNull CharSequence sequence) {
+        Objects.requireNonNull(sequence);
+
+        if (TextUtils.isEmpty(sequence)) {
+            return sequence;
+        }
+
+        final Paint paint = new Paint();
+        int trimCount = 0;
+        final int[] codePoints = sequence.codePoints().toArray();
+        for (int i = 0, length = codePoints.length; i < length; i++) {
+            String ch = Character.toString(codePoints[i]);
+            if (!isTrimmable(paint, ch)) {
+                break;
+            }
+            trimCount += ch.length();
+        }
+        if (trimCount == 0) {
+            return sequence;
+        }
+        return sequence.subSequence(trimCount, sequence.length());
+    }
+
+    /**
+     * If the {@code sequence} has some trailing trimmable characters, creates a new copy
+     * and removes the trimmable characters from the copy. Otherwise the given
+     * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
+     * to determine whether the character is trimmable or not.
+     *
+     * @return the trimmed sequence or the original sequence that has no
+     *         trailing trimmable characters.
+     * @see    #isTrimmable(Paint, CharSequence)
+     * @see    #trimStart(CharSequence)
+     * @see    #trim(CharSequence)
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    @NonNull
+    public static CharSequence trimEnd(@NonNull CharSequence sequence) {
+        Objects.requireNonNull(sequence);
+
+        if (TextUtils.isEmpty(sequence)) {
+            return sequence;
+        }
+
+        final Paint paint = new Paint();
+        int trimCount = 0;
+        final int[] codePoints = sequence.codePoints().toArray();
+        for (int i = codePoints.length - 1; i >= 0; i--) {
+            String ch = Character.toString(codePoints[i]);
+            if (!isTrimmable(paint, ch)) {
+                break;
+            }
+            trimCount += ch.length();
+        }
+
+        if (trimCount == 0) {
+            return sequence;
+        }
+        return sequence.subSequence(0, sequence.length() - trimCount);
+    }
+
+    /**
+     * If the {@code sequence} has some leading or trailing trimmable characters, creates
+     * a new copy and removes the trimmable characters from the copy. Otherwise the given
+     * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
+     * to determine whether the character is trimmable or not.
+     *
+     * @return the trimmed sequence or the original sequence that has no leading or
+     *         trailing trimmable characters.
+     * @see    #isTrimmable(Paint, CharSequence)
+     * @see    #trimStart(CharSequence)
+     * @see    #trimEnd(CharSequence)
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    @NonNull
+    public static CharSequence trim(@NonNull CharSequence sequence) {
+        Objects.requireNonNull(sequence);
+
+        if (TextUtils.isEmpty(sequence)) {
+            return sequence;
+        }
+
+        CharSequence result = trimStart(sequence);
+        if (TextUtils.isEmpty(result)) {
+            return result;
+        }
+
+        return trimEnd(result);
+    }
 }
diff --git a/core/java/android/database/sqlite/SQLiteRawStatement.java b/core/java/android/database/sqlite/SQLiteRawStatement.java
index 33f602b..c59d3ce 100644
--- a/core/java/android/database/sqlite/SQLiteRawStatement.java
+++ b/core/java/android/database/sqlite/SQLiteRawStatement.java
@@ -163,7 +163,7 @@
      * {@link IllegalStateException} if a transaction is not in progress. Clients should call
      * {@link SQLiteDatabase.createRawStatement} to create a new instance.
      */
-    SQLiteRawStatement(@NonNull SQLiteDatabase db, @NonNull String sql) throws SQLiteException {
+    SQLiteRawStatement(@NonNull SQLiteDatabase db, @NonNull String sql) {
         mThread = Thread.currentThread();
         mDatabase = db;
         mSession = mDatabase.getThreadSession();
@@ -245,7 +245,7 @@
      * @throws SQLiteDatabaseLockedException if the database is locked or busy.
      * @throws SQLiteException if a native error occurs.
      */
-    public boolean step() throws SQLiteException {
+    public boolean step() {
         throwIfInvalid();
         try {
             int err = nativeStep(mStatement, true);
@@ -392,7 +392,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindBlob(int parameterIndex, @NonNull byte[] value) throws SQLiteException {
+    public void bindBlob(int parameterIndex, @NonNull byte[] value) {
         Objects.requireNonNull(value);
         throwIfInvalid();
         try {
@@ -418,8 +418,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindBlob(int parameterIndex, @NonNull byte[] value, int offset, int length)
-            throws SQLiteException {
+    public void bindBlob(int parameterIndex, @NonNull byte[] value, int offset, int length) {
         Objects.requireNonNull(value);
         throwIfInvalid();
         throwIfInvalidBounds(value.length, offset, length);
@@ -442,7 +441,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindDouble(int parameterIndex, double value) throws SQLiteException {
+    public void bindDouble(int parameterIndex, double value) {
         throwIfInvalid();
         try {
             nativeBindDouble(mStatement, parameterIndex, value);
@@ -462,7 +461,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindInt(int parameterIndex, int value) throws SQLiteException {
+    public void bindInt(int parameterIndex, int value) {
         throwIfInvalid();
         try {
             nativeBindInt(mStatement, parameterIndex, value);
@@ -482,7 +481,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindLong(int parameterIndex, long value) throws SQLiteException {
+    public void bindLong(int parameterIndex, long value) {
         throwIfInvalid();
         try {
             nativeBindLong(mStatement, parameterIndex, value);
@@ -502,7 +501,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindNull(int parameterIndex) throws SQLiteException {
+    public void bindNull(int parameterIndex) {
         throwIfInvalid();
         try {
             nativeBindNull(mStatement, parameterIndex);
@@ -523,7 +522,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public void bindText(int parameterIndex, @NonNull String value) throws SQLiteException {
+    public void bindText(int parameterIndex, @NonNull String value) {
         Objects.requireNonNull(value);
         throwIfInvalid();
         try {
@@ -562,7 +561,7 @@
      * @throws SQLiteException if a native error occurs.
      */
     @SQLiteDataType
-    public int getColumnType(int columnIndex) throws SQLiteException {
+    public int getColumnType(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnType(mStatement, columnIndex);
@@ -584,7 +583,7 @@
      * @throws SQLiteOutOfMemoryException if the database cannot allocate memory for the name.
      */
     @NonNull
-    public String getColumnName(int columnIndex) throws SQLiteException {
+    public String getColumnName(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnName(mStatement, columnIndex);
@@ -609,7 +608,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public int getColumnLength(int columnIndex) throws SQLiteException {
+    public int getColumnLength(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnBytes(mStatement, columnIndex);
@@ -635,7 +634,7 @@
      * @throws SQLiteException if a native error occurs.
      */
     @Nullable
-    public byte[] getColumnBlob(int columnIndex) throws SQLiteException {
+    public byte[] getColumnBlob(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnBlob(mStatement, columnIndex);
@@ -668,8 +667,7 @@
      * @throws SQLiteException if a native error occurs.
      */
     public int readColumnBlob(int columnIndex, @NonNull byte[] buffer, int offset,
-            int length, int srcOffset)
-            throws SQLiteException {
+            int length, int srcOffset) {
         Objects.requireNonNull(buffer);
         throwIfInvalid();
         throwIfInvalidBounds(buffer.length, offset, length);
@@ -695,7 +693,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public double getColumnDouble(int columnIndex) throws SQLiteException {
+    public double getColumnDouble(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnDouble(mStatement, columnIndex);
@@ -719,7 +717,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public int getColumnInt(int columnIndex) throws SQLiteException {
+    public int getColumnInt(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnInt(mStatement, columnIndex);
@@ -743,7 +741,7 @@
      * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range.
      * @throws SQLiteException if a native error occurs.
      */
-    public long getColumnLong(int columnIndex) throws SQLiteException {
+    public long getColumnLong(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnLong(mStatement, columnIndex);
@@ -768,7 +766,7 @@
      * @throws SQLiteException if a native error occurs.
      */
     @NonNull
-    public String getColumnText(int columnIndex) throws SQLiteException {
+    public String getColumnText(int columnIndex) {
         throwIfInvalid();
         try {
             return nativeColumnText(mStatement, columnIndex);
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index aca6d06..5d06978 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -3402,8 +3402,8 @@
             new Key<Long>("android.sensor.exposureTime", long.class);
 
     /**
-     * <p>Duration from start of frame exposure to
-     * start of next frame exposure.</p>
+     * <p>Duration from start of frame readout to
+     * start of next frame readout.</p>
      * <p>The maximum frame rate that can be supported by a camera subsystem is
      * a function of many factors:</p>
      * <ul>
@@ -3464,6 +3464,10 @@
      * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
      * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * <p><em>Note:</em> Prior to Android 13, this field was described as measuring the duration from
+     * start of frame exposure to start of next frame exposure, which doesn't reflect the
+     * definition from sensor manufacturer. A mobile sensor defines the frame duration as
+     * intervals between sensor readouts.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }.
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1c66f82..0d204f3 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -4103,8 +4103,8 @@
             new Key<Long>("android.sensor.exposureTime", long.class);
 
     /**
-     * <p>Duration from start of frame exposure to
-     * start of next frame exposure.</p>
+     * <p>Duration from start of frame readout to
+     * start of next frame readout.</p>
      * <p>The maximum frame rate that can be supported by a camera subsystem is
      * a function of many factors:</p>
      * <ul>
@@ -4165,6 +4165,10 @@
      * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
      * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * <p><em>Note:</em> Prior to Android 13, this field was described as measuring the duration from
+     * start of frame exposure to start of next frame exposure, which doesn't reflect the
+     * definition from sensor manufacturer. A mobile sensor defines the frame duration as
+     * intervals between sensor readouts.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }.
diff --git a/core/java/android/hardware/camera2/impl/FrameNumberTracker.java b/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
index 8304796..cf496d2 100644
--- a/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
+++ b/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
@@ -90,6 +90,22 @@
                         break;
                     }
                 }
+
+                if (!removeError) {
+                    // Check for the case where we might have an error after a frame number gap
+                    // caused by other types of capture requests
+                    int otherType1 = (requestType + 1) % CaptureRequest.REQUEST_TYPE_COUNT;
+                    int otherType2 = (requestType + 2) % CaptureRequest.REQUEST_TYPE_COUNT;
+                    if (mPendingFrameNumbersWithOtherType[otherType1].isEmpty() &&
+                            mPendingFrameNumbersWithOtherType[otherType2].isEmpty()) {
+                        long errorGapNumber = Math.max(mCompletedFrameNumber[otherType1],
+                                mCompletedFrameNumber[otherType2]) + 1;
+                        if ((errorGapNumber > mCompletedFrameNumber[requestType] + 1) &&
+                                (errorGapNumber == errorFrameNumber)) {
+                            removeError = true;
+                        }
+                    }
+                }
             }
             if (removeError) {
                 mCompletedFrameNumber[requestType] = errorFrameNumber;
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl
index 0c95c2e..f6beec1 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -84,4 +84,6 @@
     boolean isReaderOptionSupported();
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)")
     boolean enableReaderOption(boolean enable);
+    boolean isObserveModeSupported();
+    boolean setObserveMode(boolean enabled);
 }
diff --git a/core/java/android/nfc/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl
index c7b3b2c..191385a 100644
--- a/core/java/android/nfc/INfcCardEmulation.aidl
+++ b/core/java/android/nfc/INfcCardEmulation.aidl
@@ -30,6 +30,7 @@
     boolean isDefaultServiceForAid(int userHandle, in ComponentName service, String aid);
     boolean setDefaultServiceForCategory(int userHandle, in ComponentName service, String category);
     boolean setDefaultForNextTap(int userHandle, in ComponentName service);
+    boolean setServiceObserveModeDefault(int userId, in android.content.ComponentName service, boolean enable);
     boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup);
     boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement);
     boolean unsetOffHostForService(int userHandle, in ComponentName service);
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index c897595..98a980f 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -1081,6 +1081,61 @@
         }
     }
 
+
+    /**
+     * Returns whether the device supports observer mode or not. When observe
+     * mode is enabled, the NFC hardware will listen for NFC readers, but not
+     * respond to them. When observe mode is disabled, the NFC hardware will
+     * resoond to the reader and proceed with the transaction.
+     * @return true if the mode is supported, false otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean isObserveModeSupported() {
+        try {
+            return sService.isObserveModeSupported();
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
+   /**
+    * Disables observe mode to allow the transaction to proceed. See
+    * {@link #isObserveModeSupported()} for a description of observe mode and
+    * use {@link #disallowTransaction()} to enable observe mode and block
+    * transactions again.
+    *
+    * @return boolean indicating success or failure.
+    */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean allowTransaction() {
+        try {
+            return sService.setObserveMode(false);
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
+    /**
+    * Signals that the transaction has completed and observe mode may be
+    * reenabled. See {@link #isObserveModeSupported()} for a description of
+    * observe mode and use {@link #allowTransaction()} to disable observe
+    * mode and allow transactions to proceed.
+    *
+    * @return boolean indicating success or failure.
+    */
+
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean disallowTransaction() {
+        try {
+            return sService.setObserveMode(true);
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
     /**
      * Resumes default polling for the current device state if polling is paused. Calling
      * this while polling is not paused is a no-op.
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index d048b59..58b6179 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -328,6 +328,24 @@
             return SELECTION_MODE_ASK_IF_CONFLICT;
         }
     }
+    /**
+     * Sets whether the system should default to observe mode or not when
+     * the service is in the foreground or the default payment service.
+     *
+     * @param service The component name of the service
+     * @param enable Whether the servic should default to observe mode or not
+     * @return whether the change was successful.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean setServiceObserveModeDefault(@NonNull ComponentName service, boolean enable) {
+        try {
+            return sService.setServiceObserveModeDefault(mContext.getUser().getIdentifier(),
+                    service, enable);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to reach CardEmulationService.");
+        }
+        return  false;
+    }
 
     /**
      * Registers a list of AIDs for a specific category for the
diff --git a/core/java/android/nfc/cardemulation/HostApduService.java b/core/java/android/nfc/cardemulation/HostApduService.java
index 55d0e73..7cd2533 100644
--- a/core/java/android/nfc/cardemulation/HostApduService.java
+++ b/core/java/android/nfc/cardemulation/HostApduService.java
@@ -16,11 +16,14 @@
 
 package android.nfc.cardemulation;
 
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.Service;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -29,6 +32,9 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * <p>HostApduService is a convenience {@link Service} class that can be
  * extended to emulate an NFC card inside an Android
@@ -230,9 +236,99 @@
     /**
      * @hide
      */
+    public static final int MSG_POLLING_LOOP = 4;
+
+    /**
+     * @hide
+     */
     public static final String KEY_DATA = "data";
 
     /**
+     * POLLING_LOOP_TYPE_KEY is the Bundle key for the type of
+     * polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
+
+    /**
+     * POLLING_LOOP_TYPE_A is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-A.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_A = 'A';
+
+    /**
+     * POLLING_LOOP_TYPE_B is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-B.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_B = 'B';
+
+    /**
+     * POLLING_LOOP_TYPE_F is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-F.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_F = 'F';
+
+    /**
+     * POLLING_LOOP_TYPE_ON is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop turns on.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_ON = 'O';
+
+    /**
+     * POLLING_LOOP_TYPE_OFF is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop turns off.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_OFF = 'X';
+
+    /**
+     * POLLING_LOOP_TYPE_UNKNOWN is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop frame isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_UNKNOWN = 'U';
+
+    /**
+     * POLLING_LOOP_DATA is the Bundle key for the raw data of captured from
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
+
+    /**
+     * POLLING_LOOP_GAIN_KEY is the Bundle key for the field strength of
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
+
+    /**
+     * POLLING_LOOP_TIMESTAMP_KEY is the Bundle key for the timestamp of
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+
+    /**
+     * @hide
+     */
+    public static final String POLLING_LOOP_FRAMES_BUNDLE_KEY =
+            "android.nfc.cardemulation.POLLING_FRAMES";
+
+    /**
      * Messenger interface to NfcService for sending responses.
      * Only accessed on main thread by the message handler.
      *
@@ -255,6 +351,7 @@
 
                 byte[] apdu = dataBundle.getByteArray(KEY_DATA);
                 if (apdu != null) {
+                        HostApduService has = HostApduService.this;
                     byte[] responseApdu = processCommandApdu(apdu, null);
                     if (responseApdu != null) {
                         if (mNfcService == null) {
@@ -306,6 +403,12 @@
                     Log.e(TAG, "RemoteException calling into NfcService.");
                 }
                 break;
+                case MSG_POLLING_LOOP:
+                    ArrayList<Bundle> frames =
+                            msg.getData().getParcelableArrayList(POLLING_LOOP_FRAMES_BUNDLE_KEY,
+                            Bundle.class);
+                    processPollingFrames(frames);
+                    break;
             default:
                 super.handleMessage(msg);
             }
@@ -366,6 +469,21 @@
         }
     }
 
+    /**
+     * This method is called when a polling frame has been received from a
+     * remote device. If the device is in observe mode, the service should
+     * call {@link NfcAdapter#allowTransaction()} once it is ready to proceed
+     * with the transaction. If the device is not in observe mode, the service
+     * can use this polling frame information to determine how to proceed if it
+     * subsequently has {@link #processCommandApdu(byte[], Bundle)} called. The
+     * service must override this method inorder to receive polling frames,
+     * otherwise the base implementation drops the frame.
+     *
+     * @param frame A description of the polling frame.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public void processPollingFrames(@NonNull List<Bundle> frame) {
+    }
 
     /**
      * <p>This method will be called when a command APDU has been received
diff --git a/core/java/android/nfc/flags.aconfig b/core/java/android/nfc/flags.aconfig
index cd50ace..17e0427 100644
--- a/core/java/android/nfc/flags.aconfig
+++ b/core/java/android/nfc/flags.aconfig
@@ -20,3 +20,31 @@
     description: "Flag for NFC user restriction"
     bug: "291187960"
 }
+
+flag {
+    name: "nfc_observe_mode"
+    namespace: "nfc"
+    description: "Enable NFC Observe Mode"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_read_polling_loop"
+    namespace: "nfc"
+    description: "Enable NFC Polling Loop Notifications"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_observe_mode_st_shim"
+    namespace: "nfc"
+    description: "Enable NFC Observe Mode ST shim"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_read_polling_loop_st_shim"
+    namespace: "nfc"
+    description: "Enable NFC Polling Loop Notifications ST shim"
+    bug: "294217286"
+}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 9956220..0ccc485 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -30,9 +30,11 @@
 import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.BinderInternal.CallSession;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
 import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
+import com.android.internal.util.Preconditions;
 
 import dalvik.annotation.optimization.CriticalNative;
 
@@ -46,6 +48,7 @@
 import java.io.PrintWriter;
 import java.lang.reflect.Modifier;
 import java.util.concurrent.atomic.AtomicReferenceArray;
+import java.util.function.Supplier;
 
 /**
  * Base class for a remotable object, the core part of a lightweight
@@ -289,6 +292,33 @@
         sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking);
     }
 
+    private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+
+    @android.ravenwood.annotation.RavenwoodKeepWholeClass
+    private static class IdentitySupplier implements Supplier<SomeArgs> {
+        @Override
+        public SomeArgs get() {
+            final SomeArgs args = SomeArgs.obtain();
+            // Match IPCThreadState behavior
+            args.arg1 = Boolean.FALSE;
+            args.argi1 = android.os.Process.myUid();
+            args.argi2 = android.os.Process.myPid();
+            return args;
+        }
+    }
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void init$ravenwood() {
+        sIdentity$ravenwood = ThreadLocal.withInitial(new IdentitySupplier());
+    }
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void reset$ravenwood() {
+        sIdentity$ravenwood = null;
+    }
+
     /**
      * Raw native pointer to JavaBBinderHolder object. Owned by this Java object. Not null.
      */
@@ -312,8 +342,14 @@
      * Warning: oneway transactions do not receive PID.
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native int getCallingPid();
 
+    /** @hide */
+    public static final int getCallingPid$ravenwood() {
+        return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().argi2;
+    }
+
     /**
      * Return the Linux UID assigned to the process that sent you the
      * current transaction that is being processed. This UID can be used with
@@ -322,8 +358,14 @@
      * incoming transaction, then its own UID is returned.
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native int getCallingUid();
 
+    /** @hide */
+    public static final int getCallingUid$ravenwood() {
+        return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().argi1;
+    }
+
     /**
      * Returns {@code true} if the current thread is currently executing an
      * incoming transaction.
@@ -331,6 +373,7 @@
      * @hide
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native boolean isDirectlyHandlingTransactionNative();
 
     /** @hide */
@@ -344,6 +387,7 @@
     /**
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isDirectlyHandlingTransaction() {
         return sIsHandlingBinderTransaction || isDirectlyHandlingTransactionNative();
     }
@@ -363,8 +407,15 @@
     * @hide
     */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     private static native boolean hasExplicitIdentity();
 
+    /** @hide */
+    private static boolean hasExplicitIdentity$ravenwood() {
+        return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().arg1
+                == Boolean.TRUE;
+    }
+
     /**
      * Return the Linux UID assigned to the process that sent the transaction
      * currently being processed.
@@ -373,6 +424,7 @@
      * executing an incoming transaction and the calling identity has not been
      * explicitly set with {@link #clearCallingIdentity()}
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final int getCallingUidOrThrow() {
         if (!isDirectlyHandlingTransaction() && !hasExplicitIdentity()) {
             throw new IllegalStateException(
@@ -434,8 +486,26 @@
      * @see #restoreCallingIdentity(long)
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native long clearCallingIdentity();
 
+    /** @hide */
+    public static final long clearCallingIdentity$ravenwood() {
+        final SomeArgs args = Preconditions.requireNonNullViaRavenwoodRule(
+                sIdentity$ravenwood).get();
+        long res = ((long) args.argi1 << 32) | args.argi2;
+        if (args.arg1 == Boolean.TRUE) {
+            res |= (0x1 << 30);
+        } else {
+            res &= ~(0x1 << 30);
+        }
+        // Match IPCThreadState behavior
+        args.arg1 = Boolean.TRUE;
+        args.argi1 = android.os.Process.myUid();
+        args.argi2 = android.os.Process.myPid();
+        return res;
+    }
+
     /**
      * Restore the identity of the incoming IPC on the current thread
      * back to a previously identity that was returned by {@link
@@ -447,8 +517,18 @@
      * @see #clearCallingIdentity
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native void restoreCallingIdentity(long token);
 
+    /** @hide */
+    public static final void restoreCallingIdentity$ravenwood(long token) {
+        final SomeArgs args = Preconditions.requireNonNullViaRavenwoodRule(
+                sIdentity$ravenwood).get();
+        args.arg1 = ((token & (0x1 << 30)) != 0) ? Boolean.TRUE : Boolean.FALSE;
+        args.argi1 = (int) (token >> 32);
+        args.argi2 = (int) (token & ~(0x1 << 30));
+    }
+
     /**
      * Convenience method for running the provided action enclosed in
      * {@link #clearCallingIdentity}/{@link #restoreCallingIdentity}.
@@ -644,8 +724,14 @@
      * in order to prevent the process from holding on to objects longer than
      * it needs to.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final native void flushPendingCommands();
 
+    /** @hide */
+    public static final void flushPendingCommands$ravenwood() {
+        // Ravenwood doesn't support IPC; ignored
+    }
+
     /**
      * Add the calling thread to the IPC thread pool. This function does
      * not return until the current process is exiting.
@@ -703,6 +789,7 @@
      * <p>If you're creating a Binder token (a Binder object without an attached interface),
      * you should use {@link #Binder(String)} instead.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public Binder() {
         this(null);
     }
@@ -719,6 +806,7 @@
      * Instead of creating multiple tokens with the same descriptor, consider adding a suffix to
      * help identify them.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public Binder(@Nullable String descriptor) {
         mObject = getNativeBBinderHolder();
         if (mObject != 0L) {
@@ -742,6 +830,7 @@
      * will be implemented for you to return the given owner IInterface when
      * the corresponding descriptor is requested.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) {
         mOwner = owner;
         mDescriptor = descriptor;
@@ -750,6 +839,7 @@
     /**
      * Default implementation returns an empty interface name.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public @Nullable String getInterfaceDescriptor() {
         return mDescriptor;
     }
@@ -758,6 +848,7 @@
      * Default implementation always returns true -- if you got here,
      * the object is alive.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean pingBinder() {
         return true;
     }
@@ -768,6 +859,7 @@
      * Note that if you're calling on a local binder, this always returns true
      * because your process is alive if you're calling it.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean isBinderAlive() {
         return true;
     }
@@ -777,6 +869,7 @@
      * to return the associated {@link IInterface} if it matches the requested
      * descriptor.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
         if (mDescriptor != null && mDescriptor.equals(descriptor)) {
             return mOwner;
@@ -1250,12 +1343,14 @@
     /**
      * Local implementation is a no-op.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
     }
 
     /**
      * Local implementation is a no-op.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) {
         return true;
     }
@@ -1283,6 +1378,7 @@
         }
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     private static native long getNativeBBinderHolder();
 
     private static long getNativeBBinderHolder$ravenwood() {
diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java
index 90e4b17..91c2965 100644
--- a/core/java/android/os/IBinder.java
+++ b/core/java/android/os/IBinder.java
@@ -194,6 +194,7 @@
      * Limit that should be placed on IPC sizes, in bytes, to keep them safely under the transaction
      * buffer limit.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     static int getSuggestedMaxIpcSizeBytes() {
         return MAX_IPC_SIZE;
     }
diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl
index f30dd20..0f27569 100644
--- a/core/java/android/os/IVibratorManagerService.aidl
+++ b/core/java/android/os/IVibratorManagerService.aidl
@@ -33,13 +33,13 @@
     boolean unregisterVibratorStateListener(int vibratorId, in IVibratorStateListener listener);
     boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
             in CombinedVibration vibration, in VibrationAttributes attributes);
-    void vibrate(int uid, int displayId, String opPkg, in CombinedVibration vibration,
+    void vibrate(int uid, int deviceId, String opPkg, in CombinedVibration vibration,
             in VibrationAttributes attributes, String reason, IBinder token);
     void cancelVibrate(int usageFilter, IBinder token);
 
     // Async oneway APIs.
     // There is no order guarantee with respect to the two-way APIs above like
     // vibrate/isVibrating/cancel.
-    oneway void performHapticFeedback(int uid, int displayId, String opPkg, int constant,
+    oneway void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
             boolean always, String reason, IBinder token);
 }
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 677143a..daec1721 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -34,6 +34,9 @@
 import android.util.Pair;
 import android.webkit.WebViewZygote;
 
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.Preconditions;
+
 import dalvik.system.VMRuntime;
 
 import libcore.io.IoUtils;
@@ -833,14 +836,37 @@
         return VMRuntime.getRuntime().is64Bit();
     }
 
+    private static SomeArgs sIdentity$ravenwood;
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void init$ravenwood(int uid, int pid) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.argi1 = uid;
+        args.argi2 = pid;
+        sIdentity$ravenwood = args;
+    }
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void reset$ravenwood() {
+        sIdentity$ravenwood = null;
+    }
+
     /**
      * Returns the identifier of this process, which can be used with
      * {@link #killProcess} and {@link #sendSignal}.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final int myPid() {
         return Os.getpid();
     }
 
+    /** @hide */
+    public static final int myPid$ravenwood() {
+        return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).argi2;
+    }
+
     /**
      * Returns the identifier of this process' parent.
      * @hide
@@ -864,10 +890,16 @@
      * app-specific sandbox.  It is different from {@link #myUserHandle} in that
      * a uid identifies a specific app sandbox in a specific user.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final int myUid() {
         return Os.getuid();
     }
 
+    /** @hide */
+    public static final int myUid$ravenwood() {
+        return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).argi1;
+    }
+
     /**
      * Returns this process's user handle.  This is the
      * user the process is running under.  It is distinct from
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index 831ca86..49a0bd3 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.location.ILocationManager;
 import android.location.LocationTime;
+import android.text.format.DateUtils;
 import android.util.Slog;
 
 import dalvik.annotation.optimization.CriticalNative;
@@ -125,6 +126,7 @@
      *
      * @param ms to sleep before returning, in milliseconds of uptime.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static void sleep(long ms)
     {
         long start = uptimeMillis();
@@ -186,8 +188,16 @@
      * @return milliseconds of non-sleep uptime since boot.
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     native public static long uptimeMillis();
 
+    /** @hide */
+    public static long uptimeMillis$ravenwood() {
+        // Ravenwood booted in Jan 2023, and has been in deep sleep for one week
+        return System.currentTimeMillis() - (1672556400L * 1_000)
+                - (DateUtils.WEEK_IN_MILLIS * 1_000);
+    }
+
     /**
      * Returns nanoseconds since boot, not counting time spent in deep sleep.
      *
@@ -195,8 +205,16 @@
      * @hide
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static native long uptimeNanos();
 
+    /** @hide */
+    public static long uptimeNanos$ravenwood() {
+        // Ravenwood booted in Jan 2023, and has been in deep sleep for one week
+        return System.nanoTime() - (1672556400L * 1_000_000_000)
+                - (DateUtils.WEEK_IN_MILLIS * 1_000_000_000);
+    }
+
     /**
      * Return {@link Clock} that starts at system boot, not counting time spent
      * in deep sleep.
@@ -218,8 +236,15 @@
      * @return elapsed milliseconds since boot.
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     native public static long elapsedRealtime();
 
+    /** @hide */
+    public static long elapsedRealtime$ravenwood() {
+        // Ravenwood booted in Jan 2023, and has been in deep sleep for one week
+        return System.currentTimeMillis() - (1672556400L * 1_000);
+    }
+
     /**
      * Return {@link Clock} that starts at system boot, including time spent in
      * sleep.
@@ -241,8 +266,15 @@
      * @return elapsed nanoseconds since boot.
      */
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static native long elapsedRealtimeNanos();
 
+    /** @hide */
+    public static long elapsedRealtimeNanos$ravenwood() {
+        // Ravenwood booted in Jan 2023, and has been in deep sleep for one week
+        return System.nanoTime() - (1672556400L * 1_000_000_000);
+    }
+
     /**
      * Returns milliseconds running in the current thread.
      *
@@ -271,8 +303,15 @@
      */
     @UnsupportedAppUsage
     @CriticalNative
+    @android.ravenwood.annotation.RavenwoodReplace
     public static native long currentTimeMicro();
 
+    /** @hide */
+    public static long currentTimeMicro$ravenwood() {
+        // Ravenwood booted in Jan 2023, and has been in deep sleep for one week
+        return System.nanoTime() / 1000L;
+    }
+
     /**
      * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized
      * using a remote network source outside the device.
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index ee90834..bc85412e 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -137,8 +137,8 @@
             return;
         }
         try {
-            mService.vibrate(uid, mContext.getAssociatedDisplayId(), opPkg, effect, attributes,
-                    reason, mToken);
+            mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason,
+                    mToken);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to vibrate.", e);
         }
@@ -152,8 +152,8 @@
         }
         try {
             mService.performHapticFeedback(
-                    Process.myUid(), mContext.getAssociatedDisplayId(), mPackageName, constant,
-                    always, reason, mToken);
+                    Process.myUid(), mContext.getDeviceId(), mPackageName, constant, always, reason,
+                    mToken);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback.", e);
         }
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 7369740..fb2ac711 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -44,6 +44,13 @@
 }
 
 flag {
+    name: "enhanced_confirmation_mode_apis"
+    namespace: "permissions"
+    description: "enable enhanced confirmation mode apis"
+    bug: "310220212"
+}
+
+flag {
   name: "op_enable_mobile_data_by_user"
   namespace: "permissions"
   description: "enables logging of the OP_ENABLE_MOBILE_DATA_BY_USER"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c012ff3..4e7734c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -19039,6 +19039,14 @@
             public static final int BATTERY_SAVER_MODE_CUSTOM = 4;
 
             /**
+             Whether 1P apps vote for enabling data during different modes,
+             i.e. BTM, BBSM
+             * @hide
+             */
+            @Readable(maxTargetSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+            public static final String CONNECTIVITY_KEEP_DATA_ON = "wear_connectivity_keep_data_on";
+
+            /**
              * The maximum ambient mode duration when an activity is allowed to auto resume.
              * @hide
              */
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index bcda25a..72c436e 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -17,6 +17,7 @@
 package android.provider;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -49,6 +50,7 @@
 import android.util.Patterns;
 
 import com.android.internal.telephony.SmsApplication;
+import com.android.internal.telephony.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -3191,8 +3193,8 @@
          * Sets whether the PDU session brought up by this APN should always be on.
          * See 3GPP TS 23.501 section 5.6.13
          * <P>Type: INTEGER</P>
-         * @hide
          */
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
         public static final String ALWAYS_ON = "always_on";
 
         /**
@@ -3302,18 +3304,16 @@
          * The MTU (maximum transmit unit) size of the mobile interface for IPv4 to which the APN is
          * connected, in bytes.
          * <p>Type: INTEGER </p>
-         * @hide
          */
-        @SystemApi
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
         public static final String MTU_V4 = "mtu_v4";
 
         /**
          * The MTU (maximum transmit unit) size of the mobile interface for IPv6 to which the APN is
          * connected, in bytes.
          * <p>Type: INTEGER </p>
-         * @hide
          */
-        @SystemApi
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
         public static final String MTU_V6 = "mtu_v6";
 
         /**
@@ -3335,17 +3335,15 @@
         /**
          * {@code true} if this APN visible to the user, {@code false} otherwise.
          * <p>Type: INTEGER (boolean)</p>
-         * @hide
          */
-        @SystemApi
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
         public static final String USER_VISIBLE = "user_visible";
 
         /**
          * {@code true} if the user allowed to edit this APN, {@code false} otherwise.
          * <p>Type: INTEGER (boolean)</p>
-         * @hide
          */
-        @SystemApi
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
         public static final String USER_EDITABLE = "user_editable";
 
         /**
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index ff4dfc7..305b751 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1004,6 +1004,8 @@
             priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
             conversationSenders = getConversationSendersWithDefault(
                     zenPolicy.getPriorityConversationSenders(), conversationSenders);
+        } else {
+            conversationSenders = CONVERSATION_SENDERS_NONE;
         }
 
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS,
@@ -1102,7 +1104,7 @@
         return (policy.suppressedVisualEffects & visualEffect) == 0;
     }
 
-    private int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
+    private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.PEOPLE_TYPE_ANYONE:
@@ -1116,7 +1118,7 @@
         }
     }
 
-    private int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
+    private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.CONVERSATION_SENDERS_ANYONE:
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
new file mode 100644
index 0000000..b63a969
--- /dev/null
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.window.flags"
+
+flag {
+  name: "allows_screen_size_decoupled_from_status_bar_and_cutout"
+  namespace: "large_screen_experiences_app_compat"
+  description: "When necessary, configuration decoupled from status bar and display cutout"
+  bug: "291870756"
+  is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 0ad6c99..b600b22 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -35,4 +35,11 @@
     name: "fullscreen_dim_flag"
     description: "Whether to allow showing fullscreen dim on ActivityEmbedding split"
     bug: "253533308"
+}
+
+flag {
+    namespace: "windowing_sdk"
+    name: "activity_embedding_interactive_divider_flag"
+    description: "Whether the interactive divider feature is enabled"
+    bug: "293654166"
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 686e1fc..9d0be4b 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -40,6 +40,7 @@
 /**
  * Static utility methods for arrays that aren't already included in {@link java.util.Arrays}.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ArrayUtils {
     private static final int CACHE_SIZE = 73;
     private static Object[] sCache = new Object[CACHE_SIZE];
@@ -48,35 +49,43 @@
 
     private ArrayUtils() { /* cannot be instantiated */ }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static byte[] newUnpaddedByteArray(int minLen) {
         return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static char[] newUnpaddedCharArray(int minLen) {
         return (char[])VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static int[] newUnpaddedIntArray(int minLen) {
         return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static boolean[] newUnpaddedBooleanArray(int minLen) {
         return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static long[] newUnpaddedLongArray(int minLen) {
         return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static float[] newUnpaddedFloatArray(int minLen) {
         return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     public static Object[] newUnpaddedObjectArray(int minLen) {
         return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @SuppressWarnings("unchecked")
     public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) {
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3496994..698c5ba 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -4303,6 +4303,9 @@
         <!-- Whether the device must be screen on before routing data to this service.
              The default is true.-->
         <attr name="requireDeviceScreenOn" format="boolean"/>
+        <!-- Whether the device should default to observe mode when this service is
+             default or in the foreground. -->
+        <attr name="defaultToObserveMode" format="boolean"/>
     </declare-styleable>
 
     <!-- Use <code>offhost-apdu-service</code> as the root tag of the XML resource that
@@ -4327,6 +4330,9 @@
         <!-- Whether the device must be screen on before routing data to this service.
              The default is false.-->
         <attr name="requireDeviceScreenOn"/>
+        <!-- Whether the device should default to observe mode when this service is
+             default or in the foreground. -->
+        <attr name="defaultToObserveMode"/>
     </declare-styleable>
 
     <!-- Specify one or more <code>aid-group</code> elements inside a
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 7642794..39d958c 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -205,6 +205,13 @@
     <string name="config_satellite_emergency_handover_intent_action" translatable="false"></string>
     <java-symbol type="string" name="config_satellite_emergency_handover_intent_action" />
 
+    <!-- Whether outgoing satellite datagrams should be sent to modem in demo mode. When satellite
+         is enabled for demo mode, if this config is enabled, outgoing datagrams will be sent to
+         modem; otherwise, success results will be returned. If demo mode is disabled, outgoing
+         datagrams are always sent to modem. -->
+    <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool>
+    <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" />
+
     <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
          will not perform handover if the target transport is out of service, or VoPS not
          supported. The network will be torn down on the source transport, and will be
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 15c188d..cec83de 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5245,6 +5245,11 @@
     <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] -->
     <string name="zen_mode_default_every_night_name">Sleeping</string>
 
+    <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_activated">On</string>
+    <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_deactivated">Off</string>
+
     <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] -->
     <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e19d548..16ad5c9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2574,6 +2574,8 @@
   <java-symbol type="string" name="zen_mode_default_weekends_name" />
   <java-symbol type="string" name="zen_mode_default_events_name" />
   <java-symbol type="string" name="zen_mode_default_every_night_name" />
+  <java-symbol type="string" name="zen_mode_implicit_activated" />
+  <java-symbol type="string" name="zen_mode_implicit_deactivated" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" />
   <java-symbol type="array" name="config_system_condition_providers" />
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 709646b..3a2e50a 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
          http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
 
     <!-- Arab Emirates -->
-    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" />
+    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214" />
 
     <!-- Albania: 5 digits, known short codes listed -->
     <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -155,7 +155,7 @@
     <shortcode country="ie" pattern="\\d{5}" premium="5[3-9]\\d{3}" free="50\\d{3}|116\\d{3}" standard="5[12]\\d{3}" />
 
     <!-- Israel: 4 digits, known premium codes listed -->
-    <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545"  free="37477" />
+    <shortcode country="il" pattern="\\d{4}" premium="4422|4545" />
 
     <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
          https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
@@ -198,9 +198,6 @@
     <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf -->
     <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668" />
 
-    <!-- Namibia: 5 digits -->
-    <shortcode country="na" pattern="\\d{1,5}" free="40005" />
-
     <!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
     <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" />
 
diff --git a/core/tests/coretests/src/android/content/pm/LauncherActivityInfoTest.java b/core/tests/coretests/src/android/content/pm/LauncherActivityInfoTest.java
new file mode 100644
index 0000000..e19c4b1
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/LauncherActivityInfoTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.content.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link android.content.pm.LauncherActivityInfo}
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LauncherActivityInfoTest {
+
+    @Test
+    public void testTrimStart() {
+        // Invisible case
+        assertThat(LauncherActivityInfo.trimStart("\u0009").toString()).isEmpty();
+        // It is not supported in the system font
+        assertThat(LauncherActivityInfo.trimStart("\u0FE1").toString()).isEmpty();
+        // Surrogates case
+        assertThat(LauncherActivityInfo.trimStart("\uD83E\uDD36").toString())
+                .isEqualTo("\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trimStart("\u0009\u0FE1\uD83E\uDD36A").toString())
+                .isEqualTo("\uD83E\uDD36A");
+        assertThat(LauncherActivityInfo.trimStart("\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\uD83E\uDD36A\u0009\u0FE1");
+        assertThat(LauncherActivityInfo.trimStart("A\uD83E\uDD36\u0009\u0FE1A").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A");
+        assertThat(LauncherActivityInfo.trimStart(
+                "A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trimStart(
+                "\u0009\u0FE1\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\uD83E\uDD36A\u0009\u0FE1");
+    }
+
+    @Test
+    public void testTrimEnd() {
+        // Invisible case
+        assertThat(LauncherActivityInfo.trimEnd("\u0009").toString()).isEmpty();
+        // It is not supported in the system font
+        assertThat(LauncherActivityInfo.trimEnd("\u0FE1").toString()).isEmpty();
+        // Surrogates case
+        assertThat(LauncherActivityInfo.trimEnd("\uD83E\uDD36").toString())
+                .isEqualTo("\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trimEnd("\u0009\u0FE1\uD83E\uDD36A").toString())
+                .isEqualTo("\u0009\u0FE1\uD83E\uDD36A");
+        assertThat(LauncherActivityInfo.trimEnd("\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\uD83E\uDD36A");
+        assertThat(LauncherActivityInfo.trimEnd("A\uD83E\uDD36\u0009\u0FE1A").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A");
+        assertThat(LauncherActivityInfo.trimEnd(
+                "A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trimEnd(
+                "\u0009\u0FE1\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\u0009\u0FE1\uD83E\uDD36A");
+    }
+
+    @Test
+    public void testTrim() {
+        // Invisible case
+        assertThat(LauncherActivityInfo.trim("\u0009").toString()).isEmpty();
+        // It is not supported in the system font
+        assertThat(LauncherActivityInfo.trim("\u0FE1").toString()).isEmpty();
+        // Surrogates case
+        assertThat(LauncherActivityInfo.trim("\uD83E\uDD36").toString())
+                .isEqualTo("\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trim("\u0009\u0FE1\uD83E\uDD36A").toString())
+                .isEqualTo("\uD83E\uDD36A");
+        assertThat(LauncherActivityInfo.trim("\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\uD83E\uDD36A");
+        assertThat(LauncherActivityInfo.trim("A\uD83E\uDD36\u0009\u0FE1A").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A");
+        assertThat(LauncherActivityInfo.trim(
+                "A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36").toString())
+                .isEqualTo("A\uD83E\uDD36\u0009\u0FE1A\uD83E\uDD36");
+        assertThat(LauncherActivityInfo.trim(
+                "\u0009\u0FE1\uD83E\uDD36A\u0009\u0FE1").toString())
+                .isEqualTo("\uD83E\uDD36A");
+    }
+}
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
index 3e3c77b..03c38cc 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
@@ -25,9 +25,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import android.annotation.EnforcePermission;
 import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.test.FakePermissionEnforcer;
 
 import androidx.test.filters.SmallTest;
 
@@ -57,7 +59,8 @@
 
     @Before
     public void setUp() {
-        mService = new TestDeviceStateManagerService();
+        FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+        mService = new TestDeviceStateManagerService(permissionEnforcer);
         mDeviceStateManagerGlobal = new DeviceStateManagerGlobal(mService);
         assertFalse(mService.mCallbacks.isEmpty());
     }
@@ -261,6 +264,10 @@
 
         private Set<IDeviceStateManagerCallback> mCallbacks = new HashSet<>();
 
+        TestDeviceStateManagerService(FakePermissionEnforcer enforcer) {
+            super(enforcer);
+        }
+
         private DeviceStateInfo getInfo() {
             final int mergedBaseState = mBaseStateRequest == null
                     ? mBaseState : mBaseStateRequest.state;
@@ -380,7 +387,10 @@
         // No-op in the test since DeviceStateManagerGlobal just calls into the system server with
         // no business logic around it.
         @Override
-        public void onStateRequestOverlayDismissed(boolean shouldCancelMode) {}
+        @EnforcePermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
+        public void onStateRequestOverlayDismissed(boolean shouldCancelMode) {
+            onStateRequestOverlayDismissed_enforcePermission();
+        }
 
         public void setSupportedStates(int[] states) {
             mSupportedStates = states;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index d5b29e3..d5fab44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -822,14 +822,23 @@
                     + "participant");
         }
 
-        // Make sure other open changes are visible as entering PIP. Some may be hidden in
-        // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+        // Make sure other non-pip changes are handled correctly.
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             if (change == enterPip) continue;
             if (TransitionUtil.isOpeningType(change.getMode())) {
+                // For other open changes that are visible when entering PIP, some may be hidden in
+                // Transitions#setupStartState because the transition type is OPEN (such as
+                // auto-enter).
                 final SurfaceControl leash = change.getLeash();
                 startTransaction.show(leash).setAlpha(leash, 1.f);
+            } else if (TransitionUtil.isClosingType(change.getMode())) {
+                // For other close changes that are invisible as entering PIP, hide them immediately
+                // to avoid showing a freezing surface.
+                // Ideally, we should let other handler to handle them (likely RemoteHandler by
+                // Launcher).
+                final SurfaceControl leash = change.getLeash();
+                startTransaction.hide(leash);
             }
         }
 
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index a1b05c1..a7d6423 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -597,7 +597,13 @@
             SkIRect clipBounds;
             if (enableClip) {
                 uirenderer::Rect initialClipBounds;
-                props.getClippingRectForFlags(props.getClippingFlags(), &initialClipBounds);
+                const auto clipFlags = props.getClippingFlags();
+                if (clipFlags) {
+                    props.getClippingRectForFlags(clipFlags, &initialClipBounds);
+                } else {
+                    // Works for RenderNode::damageSelf()
+                    initialClipBounds.set(DIRTY_MIN, DIRTY_MIN, DIRTY_MAX, DIRTY_MAX);
+                }
                 clipBounds =
                         info.damageAccumulator
                                 ->computeClipAndTransform(initialClipBounds.toSkRect(), &transform)
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index e706eb0..d55d28d 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -527,52 +527,65 @@
     return Frame(surface->logicalWidth(), surface->logicalHeight(), bufferAge);
 }
 
-struct DestroySemaphoreInfo {
+class SharedSemaphoreInfo : public LightRefBase<SharedSemaphoreInfo> {
     PFN_vkDestroySemaphore mDestroyFunction;
     VkDevice mDevice;
     VkSemaphore mSemaphore;
+    GrBackendSemaphore mGrBackendSemaphore;
 
-    DestroySemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device,
-                         VkSemaphore semaphore)
-            : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {}
+    SharedSemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device,
+                        VkSemaphore semaphore)
+            : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {
+        mGrBackendSemaphore.initVulkan(semaphore);
+    }
 
-    ~DestroySemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); }
+    ~SharedSemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); }
+
+    friend class LightRefBase<SharedSemaphoreInfo>;
+    friend class sp<SharedSemaphoreInfo>;
+
+public:
+    VkSemaphore semaphore() const { return mSemaphore; }
+
+    GrBackendSemaphore* grBackendSemaphore() { return &mGrBackendSemaphore; }
 };
 
 static void destroy_semaphore(void* context) {
-    DestroySemaphoreInfo* info = reinterpret_cast<DestroySemaphoreInfo*>(context);
-    delete info;
+    SharedSemaphoreInfo* info = reinterpret_cast<SharedSemaphoreInfo*>(context);
+    info->decStrong(0);
 }
 
 VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) {
     ATRACE_NAME("Vulkan finish frame");
 
-    VkExportSemaphoreCreateInfo exportInfo;
-    exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO;
-    exportInfo.pNext = nullptr;
-    exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
-
-    VkSemaphoreCreateInfo semaphoreInfo;
-    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
-    semaphoreInfo.pNext = &exportInfo;
-    semaphoreInfo.flags = 0;
-    VkSemaphore semaphore;
-    VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
-    ALOGE_IF(VK_SUCCESS != err, "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
-
-    GrBackendSemaphore backendSemaphore;
-    backendSemaphore.initVulkan(semaphore);
-
+    sp<SharedSemaphoreInfo> sharedSemaphore;
     GrFlushInfo flushInfo;
-    if (err == VK_SUCCESS) {
-        flushInfo.fNumSemaphores = 1;
-        flushInfo.fSignalSemaphores = &backendSemaphore;
-        flushInfo.fFinishedProc = destroy_semaphore;
-        flushInfo.fFinishedContext =
-                new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore);
-    } else {
-        semaphore = VK_NULL_HANDLE;
+
+    {
+        VkExportSemaphoreCreateInfo exportInfo;
+        exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO;
+        exportInfo.pNext = nullptr;
+        exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
+
+        VkSemaphoreCreateInfo semaphoreInfo;
+        semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+        semaphoreInfo.pNext = &exportInfo;
+        semaphoreInfo.flags = 0;
+        VkSemaphore semaphore;
+        VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
+        ALOGE_IF(VK_SUCCESS != err,
+                 "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
+
+        if (err == VK_SUCCESS) {
+            sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
+            flushInfo.fNumSemaphores = 1;
+            flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore();
+            flushInfo.fFinishedProc = destroy_semaphore;
+            sharedSemaphore->incStrong(0);
+            flushInfo.fFinishedContext = sharedSemaphore.get();
+        }
     }
+
     GrDirectContext* context = GrAsDirectContext(surface->recordingContext());
     ALOGE_IF(!context, "Surface is not backed by gpu");
     GrSemaphoresSubmitted submitted = context->flush(
@@ -581,37 +594,34 @@
     VkDrawResult drawResult{
             .submissionTime = systemTime(),
     };
-    if (semaphore != VK_NULL_HANDLE) {
-        if (submitted == GrSemaphoresSubmitted::kYes) {
-            if (mFrameBoundaryANDROID) {
-                // retrieve VkImage used as render target
-                VkImage image = VK_NULL_HANDLE;
-                GrBackendRenderTarget backendRenderTarget =
-                        SkSurfaces::GetBackendRenderTarget(
-                            surface, SkSurfaces::BackendHandleAccess::kFlushRead);
-                if (backendRenderTarget.isValid()) {
-                    GrVkImageInfo info;
-                    if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) {
-                        image = info.fImage;
-                    } else {
-                        ALOGE("Frame boundary: backend is not vulkan");
-                    }
+    if (sharedSemaphore) {
+        if (submitted == GrSemaphoresSubmitted::kYes && mFrameBoundaryANDROID) {
+            // retrieve VkImage used as render target
+            VkImage image = VK_NULL_HANDLE;
+            GrBackendRenderTarget backendRenderTarget = SkSurfaces::GetBackendRenderTarget(
+                    surface, SkSurfaces::BackendHandleAccess::kFlushRead);
+            if (backendRenderTarget.isValid()) {
+                GrVkImageInfo info;
+                if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) {
+                    image = info.fImage;
                 } else {
-                    ALOGE("Frame boundary: invalid backend render target");
+                    ALOGE("Frame boundary: backend is not vulkan");
                 }
-                // frameBoundaryANDROID needs to know about mSwapSemaphore, but
-                // it won't wait on it.
-                mFrameBoundaryANDROID(mDevice, semaphore, image);
+            } else {
+                ALOGE("Frame boundary: invalid backend render target");
             }
+            // frameBoundaryANDROID needs to know about mSwapSemaphore, but
+            // it won't wait on it.
+            mFrameBoundaryANDROID(mDevice, sharedSemaphore->semaphore(), image);
         }
         VkSemaphoreGetFdInfoKHR getFdInfo;
         getFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR;
         getFdInfo.pNext = nullptr;
-        getFdInfo.semaphore = semaphore;
+        getFdInfo.semaphore = sharedSemaphore->semaphore();
         getFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
 
         int fenceFd = -1;
-        err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
+        VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
         ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd");
         drawResult.presentFence.reset(fenceFd);
     } else {
@@ -732,15 +742,15 @@
         return INVALID_OPERATION;
     }
 
-    GrBackendSemaphore backendSemaphore;
-    backendSemaphore.initVulkan(semaphore);
+    auto sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
 
     // Even if Skia fails to submit the semaphore, it will still call the destroy_semaphore callback
     GrFlushInfo flushInfo;
     flushInfo.fNumSemaphores = 1;
-    flushInfo.fSignalSemaphores = &backendSemaphore;
+    flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore();
     flushInfo.fFinishedProc = destroy_semaphore;
-    flushInfo.fFinishedContext = new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore);
+    sharedSemaphore->incStrong(0);
+    flushInfo.fFinishedContext = sharedSemaphore.get();
     GrSemaphoresSubmitted submitted = grContext->flush(flushInfo);
     grContext->submit();
 
diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java
index 5a27435..2b349d4 100644
--- a/media/java/android/media/AudioDeviceAttributes.java
+++ b/media/java/android/media/AudioDeviceAttributes.java
@@ -325,7 +325,7 @@
                 + " role:" + roleToString(mRole)
                 + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType)
                         : AudioSystem.getInputDeviceName(mNativeType))
-                + " addr:" + mAddress
+                + " addr:" + Utils.anonymizeBluetoothAddress(mNativeType, mAddress)
                 + " name:" + mName
                 + " profiles:" + mAudioProfiles.toString()
                 + " descriptors:" + mAudioDescriptors.toString());
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 9211c53..73bc6f9 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -156,7 +156,7 @@
                             AudioSystem.getOutputDeviceName(mType));
         return "{" + super.toString()
                 + ", mType: " + type
-                + ", mAddress: " + mAddress
+                + ", mAddress: " + Utils.anonymizeBluetoothAddress(mType, mAddress)
                 + "}";
     }
 }
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index ecb6b3d..d07f611 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -657,4 +657,35 @@
         // on the fly.
         private final boolean mForceRemoveConsistency; // default false
     }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
+     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(@Nullable String address) {
+        if (address == null) {
+            return null;
+        }
+        if (address.length() != "AA:BB:CC:DD:EE:FF".length()) {
+            return address;
+        }
+        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
+    }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one if the internal device type corresponds
+     * to a Bluetooth.
+     * @param deviceType the internal type of the audio device
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(
+            int deviceType, @Nullable String address) {
+        if (!AudioSystem.isBluetoothDevice(deviceType)) {
+            return address;
+        }
+        return anonymizeBluetoothAddress(address);
+    }
 }
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
new file mode 100644
index 0000000..92cc923
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.tv.ad;
+
+/**
+ * Interface to the TV AD service.
+ * @hide
+ */
+interface ITvAdManager {
+    void startAdService(in IBinder sessionToken, int userId);
+}
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
new file mode 100644
index 0000000..b834f1b9
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.tv.ad;
+
+/**
+ * Sub-interface of ITvAdService which is created per session and has its own context.
+ * @hide
+ */
+oneway interface ITvAdSession {
+    void startAdService();
+}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
new file mode 100644
index 0000000..aa5a290
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -0,0 +1,65 @@
+/*
+ * 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.tv.ad;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Central system API to the overall client-side TV AD architecture, which arbitrates interaction
+ * between applications and AD services.
+ * @hide
+ */
+public class TvAdManager {
+    private static final String TAG = "TvAdManager";
+
+    private final ITvAdManager mService;
+    private final int mUserId;
+
+    public TvAdManager(ITvAdManager service, int userId) {
+        mService = service;
+        mUserId = userId;
+    }
+
+    /**
+     * The Session provides the per-session functionality of AD service.
+     */
+    public static final class Session {
+        private final IBinder mToken;
+        private final ITvAdManager mService;
+        private final int mUserId;
+
+        private Session(IBinder token, ITvAdManager service, int userId) {
+            mToken = token;
+            mService = service;
+            mUserId = userId;
+        }
+
+        void startAdService() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.startAdService(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
new file mode 100644
index 0000000..61101f0
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -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 android.media.tv.ad;
+
+import android.app.Service;
+import android.view.KeyEvent;
+
+/**
+ * The TvAdService class represents a TV client-side advertisement service.
+ * @hide
+ */
+public abstract class TvAdService extends Service {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvAdService";
+
+    /**
+     * Base class for derived classes to implement to provide a TV AD session.
+     */
+    public abstract static class Session implements KeyEvent.Callback {
+        /**
+         * Starts TvAdService session.
+         */
+        public void onStartAdService() {
+        }
+
+        void startAdService() {
+            onStartAdService();
+        }
+    }
+
+    /**
+     * Implements the internal ITvAdService interface.
+     */
+    public static class ITvAdSessionWrapper extends ITvAdSession.Stub {
+        private final Session mSessionImpl;
+
+        public ITvAdSessionWrapper(Session mSessionImpl) {
+            this.mSessionImpl = mSessionImpl;
+        }
+
+        @Override
+        public void startAdService() {
+            mSessionImpl.startAdService();
+        }
+    }
+}
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
new file mode 100644
index 0000000..1a3771a
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -0,0 +1,57 @@
+/*
+ * 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.tv.ad;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.ViewGroup;
+
+/**
+ * Displays contents of TV AD services.
+ * @hide
+ */
+public class TvAdView extends ViewGroup {
+    private static final String TAG = "TvAdView";
+    private static final boolean DEBUG = false;
+
+    // TODO: create session
+    private TvAdManager.Session mSession;
+
+    public TvAdView(Context context) {
+        super(context, /* attrs = */null, /* defStyleAttr = */0);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (DEBUG) {
+            Log.d(TAG,
+                    "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+        }
+    }
+
+    /**
+     * Starts the AD service.
+     */
+    public void startAdService() {
+        if (DEBUG) {
+            Log.d(TAG, "start");
+        }
+        if (mSession != null) {
+            mSession.startAdService();
+        }
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index 1c92696..223e99e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -57,19 +57,24 @@
 
 private fun UserManager.getUserGroups(): List<UserGroup> {
     val userGroupList = mutableListOf<UserGroup>()
-    val profileToShowInSettings = getProfiles(UserHandle.myUserId())
-        .map { userInfo -> userInfo to getUserProperties(userInfo.userHandle) }
+    val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT }
-        .takeIf { it.isNotEmpty() }
-        ?.map { it.first }
-        ?.let { userInfos -> userGroupList += UserGroup(userInfos) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let {
+        userGroupList += UserGroup(it)
+    }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_SEPARATE &&
-                    (!it.second.hideInSettingsInQuietMode || !it.first.isQuietModeEnabled) }
-        .forEach { userGroupList += UserGroup(userInfos = listOf(it.first)) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach {
+        userGroupList += UserGroup(listOf(it))
+    }
 
     return userGroupList
 }
+
+private fun UserManager.showInSettings(userInfo: UserInfo): Int {
+    val userProperties = getUserProperties(userInfo.userHandle)
+    return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) {
+        UserProperties.SHOW_IN_SETTINGS_NO
+    } else {
+        userProperties.showInSettings
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
new file mode 100644
index 0000000..e450364
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.settingslib.spaprivileged.template.common
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.content.pm.UserProperties
+import android.os.UserManager
+import androidx.compose.material3.Text
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+
+@RunWith(AndroidJUnit4::class)
+class UserProfilePagerTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val mockUserManager = mock<UserManager> {
+        on { getProfiles(any()) } doReturn listOf(USER_0)
+        on { getUserProperties(USER_0.userHandle) } doReturn
+            UserProperties.Builder()
+                .setShowInSettings(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
+                .build()
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { userManager } doReturn mockUserManager
+    }
+
+    @Test
+    fun userProfilePager() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                UserProfilePager { userGroup ->
+                    Text(text = userGroup.userInfos.joinToString { it.id.toString() })
+                }
+            }
+        }
+
+        composeTestRule.onNodeWithText(USER_0.id.toString()).assertIsDisplayed()
+    }
+
+    private companion object {
+        val USER_0 = UserInfo(0, "", 0)
+    }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 7e8fe7e..d9fe733 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -445,5 +445,6 @@
         VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_SUPPORTED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WEAR_LAUNCHER_UI_MODE, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 2e174e2..c0d83c4 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -686,7 +686,8 @@
                     Settings.Global.Wearable.PHONE_SWITCHING_SUPPORTED,
                     Settings.Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE,
                     Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE,
-                    Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED);
+                    Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED,
+                    Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON);
 
     private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS =
              newHashSet(
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 0e9f8b1..80fd516 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -294,8 +294,6 @@
         "tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt",
-        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt",
-        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt",
         // Keyguard helper
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index aebdaab..e340209 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -107,8 +107,16 @@
 }
 
 flag {
-   name: "qs_new_pipeline"
-   namespace: "systemui"
-   description: "Use the new pipeline for Quick Settings. Should have no behavior changes."
-   bug: "241772429"
+    name: "qs_new_pipeline"
+    namespace: "systemui"
+    description: "Use the new pipeline for Quick Settings. Should have no behavior changes."
+    bug: "241772429"
 }
+
+flag {
+    name: "coroutine_tracing"
+    namespace: "systemui"
+    description: "Adds thread-local data to System UI's global coroutine scopes to "
+        "allow for tracing of coroutine continuations using System UI's tracinglib"
+    bug: "289353932"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 3928767..7eb7dac 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -208,12 +208,8 @@
     val isSplitAroundTheFoldRequired by viewModel.isFoldSplitRequired.collectAsState()
     val isSplitAroundTheFold =
         foldPosture == FoldPosture.Tabletop && !outputOnly && isSplitAroundTheFoldRequired
-    val currentSceneKey by
-        remember(isSplitAroundTheFold) {
-            mutableStateOf(
-                if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
-            )
-        }
+    val currentSceneKey =
+        if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
 
     SceneTransitionLayout(
         currentScene = currentSceneKey,
@@ -405,8 +401,7 @@
             if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
                 PatternBouncer(
                     viewModel = nonNullViewModel,
-                    modifier =
-                        Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false).then(modifier)
+                    modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
                 )
             }
         else -> Unit
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 fb50f69..243751fa 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
@@ -76,6 +76,8 @@
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
     val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState()
     val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+    val isDigitButtonAnimationEnabled: Boolean by
+        viewModel.isDigitButtonAnimationEnabled.collectAsState()
 
     val buttonScaleAnimatables = remember { List(12) { Animatable(1f) } }
     LaunchedEffect(animateFailure) {
@@ -94,10 +96,11 @@
     ) {
         repeat(9) { index ->
             DigitButton(
-                index + 1,
-                isInputEnabled,
-                viewModel::onPinButtonClicked,
-                buttonScaleAnimatables[index]::value,
+                digit = index + 1,
+                isInputEnabled = isInputEnabled,
+                onClicked = viewModel::onPinButtonClicked,
+                scaling = buttonScaleAnimatables[index]::value,
+                isAnimationEnabled = isDigitButtonAnimationEnabled,
             )
         }
 
@@ -116,10 +119,11 @@
         )
 
         DigitButton(
-            0,
-            isInputEnabled,
-            viewModel::onPinButtonClicked,
-            buttonScaleAnimatables[10]::value,
+            digit = 0,
+            isInputEnabled = isInputEnabled,
+            onClicked = viewModel::onPinButtonClicked,
+            scaling = buttonScaleAnimatables[10]::value,
+            isAnimationEnabled = isDigitButtonAnimationEnabled,
         )
 
         ActionButton(
@@ -143,15 +147,17 @@
     isInputEnabled: Boolean,
     onClicked: (Int) -> Unit,
     scaling: () -> Float,
+    isAnimationEnabled: Boolean,
 ) {
     PinPadButton(
         onClicked = { onClicked(digit) },
         isEnabled = isInputEnabled,
         backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
         foregroundColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        isAnimationEnabled = isAnimationEnabled,
         modifier =
             Modifier.graphicsLayer {
-                val scale = scaling()
+                val scale = if (isAnimationEnabled) scaling() else 1f
                 scaleX = scale
                 scaleY = scale
             }
@@ -195,6 +201,7 @@
         isEnabled = isInputEnabled && !isHidden,
         backgroundColor = backgroundColor,
         foregroundColor = foregroundColor,
+        isAnimationEnabled = true,
         modifier =
             Modifier.graphicsLayer {
                 alpha = hiddenAlpha
@@ -216,6 +223,7 @@
     isEnabled: Boolean,
     backgroundColor: Color,
     foregroundColor: Color,
+    isAnimationEnabled: Boolean,
     modifier: Modifier = Modifier,
     onLongPressed: (() -> Unit)? = null,
     content: @Composable (contentColor: () -> Color) -> Unit,
@@ -243,7 +251,7 @@
 
     val cornerRadius: Dp by
         animateDpAsState(
-            if (isPressed) 24.dp else pinButtonSize / 2,
+            if (isAnimationEnabled && isPressed) 24.dp else pinButtonSize / 2,
             label = "PinButton round corners",
             animationSpec = tween(animDurationMillis, easing = animEasing)
         )
@@ -251,7 +259,7 @@
     val containerColor: Color by
         animateColorAsState(
             when {
-                isPressed -> MaterialTheme.colorScheme.primary
+                isAnimationEnabled && isPressed -> MaterialTheme.colorScheme.primary
                 else -> backgroundColor
             },
             label = "Pin button container color",
@@ -260,7 +268,7 @@
     val contentColor =
         animateColorAsState(
             when {
-                isPressed -> MaterialTheme.colorScheme.onPrimary
+                isAnimationEnabled && isPressed -> MaterialTheme.colorScheme.onPrimary
                 else -> foregroundColor
             },
             label = "Pin button container color",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 1429782..0569431 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -25,7 +25,6 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
@@ -164,11 +163,7 @@
 @Composable
 private fun Umo(viewModel: CommunalViewModel, modifier: Modifier = Modifier) {
     AndroidView(
-        modifier =
-            modifier
-                .width(Dimensions.CardWidth)
-                .height(Dimensions.CardHeightThird)
-                .padding(Dimensions.Spacing),
+        modifier = modifier,
         factory = {
             viewModel.mediaHost.expansion = MediaHostState.EXPANDED
             viewModel.mediaHost.showsOnlyActiveMedia = false
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index aae61bd..b77a60b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -506,7 +506,10 @@
 
     // There is no ongoing transition.
     if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
-        return idleValue
+        // Even if this element SceneTransitionLayout is not animated, the layout itself might be
+        // animated (e.g. by another parent SceneTransitionLayout), in which case this element still
+        // need to participate in the layout phase.
+        return currentValue()
     }
 
     // A transition was started but it's not ready yet (not all elements have been composed/laid
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index 216608a..5d8eaf7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,9 +2,10 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.unit.IntSize
 
 interface DraggableHandler {
-    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
+    fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1)
     fun onDelta(pixels: Float)
     fun onDragStopped(velocity: Float)
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index d0a5f5b..d48781a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastForEach
 
@@ -60,7 +61,7 @@
     orientation: Orientation,
     enabled: Boolean,
     startDragImmediately: Boolean,
-    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
+    onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
     onDragDelta: (Float) -> Unit,
     onDragStopped: (velocity: Float) -> Unit,
 ): Modifier = composed {
@@ -83,7 +84,7 @@
 
         val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
             velocityTracker.resetTracking()
-            onDragStarted(startedPosition, pointersDown)
+            onDragStarted(size, startedPosition, pointersDown)
         }
 
         val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index eb5168b..1a79522 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -25,8 +25,9 @@
 import androidx.compose.runtime.mutableStateOf
 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.layout.onPlaced
+import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
@@ -44,14 +45,24 @@
     var content by mutableStateOf(content)
     var userActions by mutableStateOf(actions)
     var zIndex by mutableFloatStateOf(zIndex)
-    var size by mutableStateOf(IntSize.Zero)
+    var targetSize by mutableStateOf(IntSize.Zero)
 
     /** The shared values in this scene that are not tied to a specific element. */
     val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()
 
     @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
     fun Content(modifier: Modifier = Modifier) {
-        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) {
+        Box(
+            modifier
+                .zIndex(zIndex)
+                .intermediateLayout { measurable, constraints ->
+                    targetSize = lookaheadSize
+                    val placeable = measurable.measure(constraints)
+                    layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+                }
+                .testTag(key.testTag)
+        ) {
             scope.content()
         }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 9a3a0ae..9d71801 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
@@ -90,7 +91,7 @@
 
     internal var gestureWithPriority: Any? = null
 
-    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+    internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) {
         if (isDrivingTransition) {
             // This [transition] was already driving the animation: simply take over it.
             // Stop animating and start from where the current offset.
@@ -126,14 +127,14 @@
         // we will also have to make sure that we correctly handle overscroll.
         swipeTransition.absoluteDistance =
             when (orientation) {
-                Orientation.Horizontal -> layoutImpl.size.width
-                Orientation.Vertical -> layoutImpl.size.height
+                Orientation.Horizontal -> layoutSize.width
+                Orientation.Vertical -> layoutSize.height
             }.toFloat()
 
         val fromEdge =
             startedPosition?.let { position ->
                 layoutImpl.edgeDetector.edge(
-                    layoutImpl.size,
+                    layoutSize,
                     position.round(),
                     layoutImpl.density,
                     orientation,
@@ -513,9 +514,9 @@
 private class SceneDraggableHandler(
     private val gestureHandler: SceneGestureHandler,
 ) : DraggableHandler {
-    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+    override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) {
         gestureHandler.gestureWithPriority = this
-        gestureHandler.onDragStarted(pointersDown, startedPosition)
+        gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition)
     }
 
     override fun onDelta(pixels: Float) {
@@ -647,7 +648,11 @@
             canContinueScroll = { true },
             onStart = {
                 gestureHandler.gestureWithPriority = this
-                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+                gestureHandler.onDragStarted(
+                    pointersDown = 1,
+                    layoutSize = gestureHandler.currentScene.targetSize,
+                    startedPosition = null,
+                )
             },
             onScroll = { offsetAvailable ->
                 if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 6edd1b6..0b06953 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -30,13 +30,15 @@
 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.layout.intermediateLayout
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
+import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 
@@ -64,12 +66,6 @@
     private val horizontalGestureHandler: SceneGestureHandler
     private val verticalGestureHandler: SceneGestureHandler
 
-    /**
-     * 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.
-     */
-    @VisibleForTesting var size by mutableStateOf(IntSize.Zero)
-
     init {
         setScenes(builder)
 
@@ -157,15 +153,46 @@
     }
 
     @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(gestureHandler(Orientation.Horizontal))
-                .swipeToScene(gestureHandler(Orientation.Vertical))
-                .onSizeChanged { size = it }
+                .swipeToScene(horizontalGestureHandler)
+                .swipeToScene(verticalGestureHandler)
+                // Animate the size of this layout.
+                .intermediateLayout { measurable, constraints ->
+                    // Measure content normally.
+                    val placeable = measurable.measure(constraints)
+
+                    val width: Int
+                    val height: Int
+                    val state = state.transitionState
+                    if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+                        width = placeable.width
+                        height = placeable.height
+                    } else {
+                        // Interpolate the size.
+                        val fromSize = scene(state.fromScene).targetSize
+                        val toSize = scene(state.toScene).targetSize
+
+                        // Optimization: make sure we don't read state.progress if fromSize ==
+                        // toSize to avoid running this code every frame when the layout size does
+                        // not change.
+                        if (fromSize == toSize) {
+                            width = fromSize.width
+                            height = fromSize.height
+                        } else {
+                            val size = lerp(fromSize, toSize, state.progress)
+                            width = size.width.coerceAtLeast(0)
+                            height = size.height.coerceAtLeast(0)
+                        }
+                    }
+
+                    layout(width, height) { placeable.place(0, 0) }
+                }
         ) {
             LookaheadScope {
                 val scenesToCompose =
@@ -230,4 +257,9 @@
     }
 
     internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
+
+    @VisibleForTesting
+    fun setScenesTargetSizeForTest(size: IntSize) {
+        scenes.values.forEach { it.targetSize = size }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 840800d..70534dd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -38,7 +38,7 @@
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = scene.size
+        val sceneSize = scene.targetSize
         val elementSize = sceneValues.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1e3d011..7ab2096 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -46,6 +46,7 @@
 import org.junit.runner.RunWith
 
 private const val SCREEN_SIZE = 100f
+private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
 
 @RunWith(AndroidJUnit4::class)
 class SceneGestureHandlerTest {
@@ -80,7 +81,7 @@
                             edgeDetector = DefaultEdgeDetector,
                             coroutineScope = coroutineScope,
                         )
-                        .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
+                        .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
                 orientation = Orientation.Vertical,
                 coroutineScope = coroutineScope,
             )
@@ -128,18 +129,21 @@
         runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
     }
 
+    private fun DraggableHandler.onDragStarted() =
+        onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero)
+
     @Test
     fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) }
 
     @Test
     fun onDragStarted_shouldStartATransition() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
@@ -152,7 +156,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -170,7 +174,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -188,7 +192,7 @@
 
     @Test
     fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(velocity = 0f)
@@ -197,7 +201,7 @@
 
     @Test
     fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -217,7 +221,7 @@
         assertScene(currentScene = SceneC, isIdle = false)
 
         // Start a new gesture while the offset is animating
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
     }
 
@@ -421,6 +425,7 @@
         draggable.onDelta(deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = true)
     }
+
     @Test
     fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
         draggable.onDragStopped(velocityThreshold)
@@ -437,7 +442,7 @@
     @Test
     fun startNestedScrollWhileDragging() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
-        draggable.onDragStarted(Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 5afd420..321cf63 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -18,6 +18,8 @@
 
 import androidx.activity.ComponentActivity
 import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -48,6 +50,7 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.assertSizeIsEqualTo
 import com.android.compose.test.subjects.DpOffsetSubject
 import com.android.compose.test.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
@@ -307,6 +310,26 @@
         assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
     }
 
+    @Test
+    fun layoutSizeIsAnimated() {
+        val layoutTag = "layout"
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.size(200.dp, 100.dp)) },
+            toSceneContent = { Box(Modifier.size(120.dp, 140.dp)) },
+            transition = {
+                // 4 frames of animation.
+                spec = tween(4 * 16, easing = LinearEasing)
+            },
+            layoutModifier = Modifier.testTag(layoutTag),
+        ) {
+            before { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(200.dp, 100.dp) }
+            at(16) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(180.dp, 110.dp) }
+            at(32) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(160.dp, 120.dp) }
+            at(48) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(140.dp, 130.dp) }
+            after { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(120.dp, 140.dp) }
+        }
+    }
+
     private fun SemanticsNodeInteraction.offsetRelativeTo(
         other: SemanticsNodeInteraction,
     ): DpOffset {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
index 2a27763..8cffcf6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -48,7 +48,7 @@
         rule.testTransition(
             // The layout under test is 300dp x 300dp.
             layoutModifier = Modifier.size(300.dp),
-            fromSceneContent = {},
+            fromSceneContent = { Box(Modifier.fillMaxSize()) },
             toSceneContent = {
                 // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
                 Box(Modifier.fillMaxSize()) {
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index e0ae1be..06de296 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -16,7 +16,6 @@
 
 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
@@ -104,7 +103,7 @@
                 currentScene,
                 onChangeScene,
                 transitions { from(fromScene, to = toScene, transition) },
-                layoutModifier.fillMaxSize(),
+                layoutModifier,
             ) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
diff --git a/packages/SystemUI/res/drawable/ic_ksh_key_meta.xml b/packages/SystemUI/res/drawable/ic_ksh_key_meta.xml
index be8fe8c..ff1146e 100644
--- a/packages/SystemUI/res/drawable/ic_ksh_key_meta.xml
+++ b/packages/SystemUI/res/drawable/ic_ksh_key_meta.xml
@@ -19,9 +19,14 @@
         android:height="24dp"
         android:viewportWidth="24"
         android:viewportHeight="24">
-    <path android:fillColor="@color/ksh_key_item_color"
-            android:pathData="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91
-3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27 .28 v.79l5 4.99L20.49
-19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5
-14z" />
+    <path android:pathData="M5.5,5.5m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+            android:fillColor="@color/ksh_key_item_color" />
+    <path android:pathData="M5.5,16.5m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+            android:fillColor="@color/ksh_key_item_color" />
+    <path android:pathData="M16.5,5.5m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+            android:fillColor="@color/ksh_key_item_color" />
+    <path android:pathData="M18.5,16.5C18.5,15.4 17.6,14.5 16.5,14.5C15.4,14.5 14.5,15.4 14.5,16.5C14.5,17.6 15.4,18.5 16.5,18.5C17.6,18.5 18.5,17.6 18.5,16.5ZM12.5,16.5C12.5,14.29 14.29,12.5 16.5,12.5C18.71,12.5 20.5,14.29 20.5,16.5C20.5,17.241 20.299,17.934 19.948,18.529L23,21.59L21.59,23L18.529,19.948C17.934,20.299 17.241,20.5 16.5,20.5C14.29,20.5 12.5,18.71 12.5,16.5Z"
+            android:fillColor="@color/ksh_key_item_color"
+            android:fillType="evenOdd" />
 </vector>
+
diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
index 6082fb9..8ad32b4 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
@@ -25,7 +25,11 @@
     UDFPS_ULTRASONIC,
     UDFPS_OPTICAL,
     POWER_BUTTON,
-    HOME_BUTTON
+    HOME_BUTTON;
+
+    fun isUdfps(): Boolean {
+        return (this == UDFPS_OPTICAL) || (this == UDFPS_ULTRASONIC)
+    }
 }
 
 /** Convert [this] to corresponding [FingerprintSensorType] */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 32f9c30..701f7e5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -62,6 +62,7 @@
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
@@ -105,6 +106,7 @@
     private final DozeParameters mDozeParameters;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore;
+    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -179,6 +181,7 @@
             LockscreenSmartspaceController smartspaceController,
             ConfigurationController configurationController,
             ScreenOffAnimationController screenOffAnimationController,
+            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
@@ -202,6 +205,7 @@
         mSmartspaceController = smartspaceController;
         mConfigurationController = configurationController;
         mScreenOffAnimationController = screenOffAnimationController;
+        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
@@ -605,6 +609,7 @@
                             mAodIconsViewModel,
                             mConfigurationState,
                             mConfigurationController,
+                            mIconViewBindingFailureTracker,
                             mAodIconViewStore);
                     final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility(
                             nic,
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 ee3a55f..7769dd9 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
@@ -108,6 +108,9 @@
     /** The minimal length of a pattern. */
     val minPatternLength: Int
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean>
+
     /**
      * Returns the currently-configured authentication method. This determines how the
      * authentication challenge needs to be completed in order to unlock an otherwise locked device.
@@ -212,6 +215,12 @@
 
     override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE
 
+    override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        refreshingFlow(
+            initialValue = true,
+            getFreshValue = { userId -> lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) },
+        )
+
     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
         return withContext(backgroundDispatcher) {
             blockingAuthenticationMethodInternal(userRepository.selectedUserId)
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 1ede530..5eefbf5 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
@@ -145,6 +145,9 @@
     val authenticationChallengeResult: SharedFlow<Boolean> =
         repository.authenticationChallengeResult
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled
+
     private var throttlingCountdownJob: Job? = null
 
     init {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 4e1cddc..ff36839 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -92,6 +92,10 @@
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
 
+    /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
+    val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        authenticationInteractor.isPinEnhancedPrivacyEnabled
+
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
     val isUserSwitcherVisible: Boolean
         get() = repository.isUserSwitcherVisible
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 2ed0d5d..b2b8049 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
@@ -84,6 +84,19 @@
 
     override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
+    /**
+     * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
+     * delete or enter buttons; those should always animate.
+     */
+    val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
+        interactor.isPinEnhancedPrivacyEnabled
+            .map { !it }
+            .stateIn(
+                scope = viewModelScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = !interactor.isPinEnhancedPrivacyEnabled.value,
+            )
+
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
         val pinInput = mutablePinInput.value
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 508f52c..771dfbc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -141,6 +141,7 @@
     private val umoContent: Flow<List<CommunalContentModel.Umo>> =
         mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying ->
             if (mediaPlaying) {
+                // TODO(b/310254801): support HALF and FULL layouts
                 flowOf(listOf(CommunalContentModel.Umo(CommunalContentSize.THIRD)))
             } else {
                 flowOf(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 9fc86ad..1f2621d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -120,6 +120,7 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
 import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
+import com.android.systemui.statusbar.ui.binder.StatusBarViewBinderModule;
 import com.android.systemui.statusbar.window.StatusBarWindowModule;
 import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
 import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
@@ -215,6 +216,7 @@
         StatusBarModule.class,
         StatusBarPipelineModule.class,
         StatusBarPolicyModule.class,
+        StatusBarViewBinderModule.class,
         StatusBarWindowModule.class,
         SystemPropertiesFlagsModule.class,
         SysUIConcurrencyModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index c3f3529..298811b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collectLatest
@@ -46,6 +47,7 @@
  * Device entry occurs when the user successfully dismisses (or bypasses) the lockscreen, regardless
  * of the authentication method used.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class DeviceEntryInteractor
 @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractor.kt
new file mode 100644
index 0000000..72b9da6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractor.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.deviceentry.domain.interactor
+
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Encapsulates business logic for device entry under-display fingerprint state changes. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class DeviceEntryUdfpsInteractor
+@Inject
+constructor(
+    // TODO (b/309655554): create & use interactors for these repositories
+    fingerprintPropertyRepository: FingerprintPropertyRepository,
+    fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    biometricSettingsRepository: BiometricSettingsRepository,
+) {
+    /** Whether the device supports an under display fingerprint sensor. */
+    val isUdfpsSupported: Flow<Boolean> =
+        fingerprintPropertyRepository.sensorType.map { it.isUdfps() }
+
+    /** Whether the under-display fingerprint sensor is enrolled and enabled for device entry. */
+    val isUdfpsEnrolledAndEnabled: Flow<Boolean> =
+        combine(isUdfpsSupported, biometricSettingsRepository.isFingerprintEnrolledAndEnabled) {
+            udfps,
+            fpEnrolledAndEnabled ->
+            udfps && fpEnrolledAndEnabled
+        }
+    /** Whether the under display fingerprint sensor is currently running. */
+    val isListeningForUdfps =
+        isUdfpsSupported.flatMapLatest { isUdfps ->
+            if (isUdfps) {
+                fingerprintAuthRepository.isRunning
+            } else {
+                flowOf(false)
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index d64a131..cd3ecb3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -316,11 +316,6 @@
     val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
         releasedFlag("smartspace_shared_element_transition_enabled")
 
-    // TODO(b/258517050): Clean up after the feature is launched.
-    @JvmField
-    val SMARTSPACE_DATE_WEATHER_DECOUPLED =
-        sysPropBooleanFlag("persist.sysui.ss.dw_decoupled", default = true)
-
     // TODO(b/270223352): Tracking Bug
     @JvmField
     val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag("hide_smartspace_on_dream_overlay")
@@ -441,9 +436,6 @@
     // TODO(b/270437894): Tracking Bug
     val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume")
 
-    // TODO(b/304506662): Tracking Bug
-    val MEDIA_DEVICE_NAME_FIX = releasedFlag("media_device_name_fix")
-
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 8b93b17..331d892 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -55,6 +55,7 @@
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransitionModule;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -94,6 +95,7 @@
         KeyguardStatusViewComponent.class,
         KeyguardUserSwitcherComponent.class},
         includes = {
+            DeviceEntryIconTransitionModule.class,
             FalsingModule.class,
             KeyguardDataQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
index 8bf2bc3..cc1cf91 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
@@ -50,14 +50,18 @@
     private val configurationRepository: ConfigurationRepository,
     private val keyguardInteractor: KeyguardInteractor,
 ) {
-    val udfpsBurnInXOffset: StateFlow<Int> =
+    val deviceEntryIconXOffset: StateFlow<Int> =
         burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_x, isXAxis = true)
-    val udfpsBurnInYOffset: StateFlow<Int> =
+    val deviceEntryIconYOffset: StateFlow<Int> =
         burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_y, isXAxis = false)
-    val udfpsBurnInProgress: StateFlow<Float> =
+    val udfpsProgress: StateFlow<Float> =
         keyguardInteractor.dozeTimeTick
             .mapLatest { burnInHelperWrapper.burnInProgressOffset() }
-            .stateIn(scope, SharingStarted.Lazily, burnInHelperWrapper.burnInProgressOffset())
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                burnInHelperWrapper.burnInProgressOffset()
+            )
 
     val keyguardBurnIn: Flow<BurnInModel> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index a331a66..8584401 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -113,7 +113,9 @@
     }
 
     companion object {
-        val TO_LOCKSCREEN_DURATION = 500.milliseconds
         private val DEFAULT_DURATION = 500.milliseconds
+        val TO_LOCKSCREEN_DURATION = 500.milliseconds
+        val TO_GONE_DURATION = DEFAULT_DURATION
+        val TO_OCCLUDED_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index e9719e7..eca7088 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -26,11 +26,11 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
-import javax.inject.Inject
-import kotlin.time.Duration.Companion.milliseconds
 
 @SysUISingleton
 class FromDozingTransitionInteractor
@@ -97,5 +97,6 @@
 
     companion object {
         private val DEFAULT_DURATION = 500.milliseconds
+        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index eace0c7..bd73d60 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -138,6 +138,6 @@
     companion object {
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_DREAMING_DURATION = 933.milliseconds
-        val TO_AOD_DURATION = 1100.milliseconds
+        val TO_AOD_DURATION = 1300.milliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index ea40ba0..152d217 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -391,5 +391,7 @@
         val TO_DREAMING_DURATION = 933.milliseconds
         val TO_OCCLUDED_DURATION = 450.milliseconds
         val TO_AOD_DURATION = 500.milliseconds
+        val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
+        val TO_GONE_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index dec38b5..6a8555c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -142,9 +142,7 @@
                     ::toTriple
                 )
                 .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (
-                        lastStartedStep.to == KeyguardState.OCCLUDED && isAsleep
-                    ) {
+                    if (lastStartedStep.to == KeyguardState.OCCLUDED && isAsleep) {
                         startTransitionTo(
                             if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
                         )
@@ -187,5 +185,6 @@
     companion object {
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 933.milliseconds
+        val TO_AOD_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 24b6661..5f246e1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -255,5 +255,7 @@
         private val DEFAULT_DURATION = 300.milliseconds
         val TO_GONE_DURATION = 500.milliseconds
         val TO_GONE_SHORT_DURATION = 200.milliseconds
+        val TO_AOD_DURATION = DEFAULT_DURATION
+        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
index c0308e6..f5cd767 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
@@ -51,14 +51,14 @@
     val scaleForResolution = configRepo.scaleForResolution
 
     /** Burn-in offsets for the UDFPS view to mitigate burn-in on AOD. */
-    val burnInOffsets: Flow<BurnInOffsets> =
+    val burnInOffsets: Flow<Offsets> =
         combine(
             keyguardInteractor.dozeAmount,
-            burnInInteractor.udfpsBurnInXOffset,
-            burnInInteractor.udfpsBurnInYOffset,
-            burnInInteractor.udfpsBurnInProgress
+            burnInInteractor.deviceEntryIconXOffset,
+            burnInInteractor.deviceEntryIconYOffset,
+            burnInInteractor.udfpsProgress
         ) { dozeAmount, fullyDozingBurnInX, fullyDozingBurnInY, fullyDozingBurnInProgress ->
-            BurnInOffsets(
+            Offsets(
                 intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInX),
                 intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInY),
                 floatEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInProgress),
@@ -86,8 +86,8 @@
             .onStart { emit(0f) }
 }
 
-data class BurnInOffsets(
-    val burnInXOffset: Int, // current x burn in offset based on the aodTransitionAmount
-    val burnInYOffset: Int, // current y burn in offset based on the aodTransitionAmount
-    val burnInProgress: Float, // current progress based on the aodTransitionAmount
+data class Offsets(
+    val x: Int, // current x burn in offset based on the aodTransitionAmount
+    val y: Int, // current y burn in offset based on the aodTransitionAmount
+    val progress: Float, // current progress based on the aodTransitionAmount
 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 9d7477c..d5ad7ab 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -105,4 +105,9 @@
             }
             .filterNotNull()
     }
+
+    /** Immediately (after 1ms) emits the given value for every step of the KeyguardTransition. */
+    fun immediatelyTransitionTo(value: Float): Flow<Float> {
+        return createFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index e82ea7f..a8b28bc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -24,6 +24,8 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.common.ui.view.LongPressHandlingView
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
@@ -34,19 +36,24 @@
 object DeviceEntryIconViewBinder {
 
     /**
-     * Updates UI for the device entry icon view (lock, unlock and fingerprint icons) and its
-     * background.
+     * Updates UI for:
+     * - device entry containing view (parent view for the below views)
+     *     - long-press handling view (transparent, no UI)
+     *     - foreground icon view (lock/unlock/fingerprint)
+     *     - background view (optional)
      */
     @SuppressLint("ClickableViewAccessibility")
     @JvmStatic
     fun bind(
         view: DeviceEntryIconView,
         viewModel: DeviceEntryIconViewModel,
+        fgViewModel: DeviceEntryForegroundViewModel,
+        bgViewModel: DeviceEntryBackgroundViewModel,
         falsingManager: FalsingManager,
     ) {
-        val iconView = view.iconView
-        val bgView = view.bgView
         val longPressHandlingView = view.longPressHandlingView
+        val fgIconView = view.iconView
+        val bgView = view.bgView
         longPressHandlingView.listener =
             object : LongPressHandlingView.Listener {
                 override fun onLongPressDetected(view: View, x: Int, y: Int) {
@@ -56,37 +63,12 @@
                     viewModel.onLongPress()
                 }
             }
+
         view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.iconViewModel.collect { iconViewModel ->
-                        iconView.setImageState(
-                            view.getIconState(iconViewModel.type, iconViewModel.useAodVariant),
-                            /* merge */ false
-                        )
-                        iconView.imageTintList = ColorStateList.valueOf(iconViewModel.tint)
-                        iconView.alpha = iconViewModel.alpha
-                        iconView.setPadding(
-                            iconViewModel.padding,
-                            iconViewModel.padding,
-                            iconViewModel.padding,
-                            iconViewModel.padding,
-                        )
-                    }
-                }
-                launch {
-                    viewModel.backgroundViewModel.collect { bgViewModel ->
-                        bgView.alpha = bgViewModel.alpha
-                        bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
-                    }
-                }
-                launch {
-                    viewModel.burnInViewModel.collect { burnInViewModel ->
-                        view.translationX = burnInViewModel.x.toFloat()
-                        view.translationY = burnInViewModel.y.toFloat()
-                        view.aodFpDrawable.progress = burnInViewModel.progress
-                    }
-                }
+            // Repeat on CREATED so that the view will always observe the entire
+            // GONE => AOD transition (even though the view may not be visible until the middle
+            // of the transition.
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
                     viewModel.isLongPressEnabled.collect { isEnabled ->
                         longPressHandlingView.setLongPressHandlingEnabled(isEnabled)
@@ -97,6 +79,55 @@
                         view.accessibilityHintType = hint
                     }
                 }
+                launch {
+                    viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
+                        if (useBackgroundProtection) {
+                            bgView.visibility = View.VISIBLE
+                        } else {
+                            bgView.visibility = View.GONE
+                        }
+                    }
+                }
+                launch {
+                    viewModel.burnInOffsets.collect { burnInOffsets ->
+                        view.translationX = burnInOffsets.x.toFloat()
+                        view.translationY = burnInOffsets.y.toFloat()
+                        view.aodFpDrawable.progress = burnInOffsets.progress
+                    }
+                }
+
+                launch { viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha } }
+            }
+        }
+
+        fgIconView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    fgViewModel.viewModel.collect { viewModel ->
+                        fgIconView.setImageState(
+                            view.getIconState(viewModel.type, viewModel.useAodVariant),
+                            /* merge */ false
+                        )
+                        fgIconView.imageTintList = ColorStateList.valueOf(viewModel.tint)
+                        fgIconView.setPadding(
+                            viewModel.padding,
+                            viewModel.padding,
+                            viewModel.padding,
+                            viewModel.padding,
+                        )
+                    }
+                }
+            }
+        }
+
+        bgView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    bgViewModel.viewModel.collect { bgViewModel ->
+                        bgView.alpha = bgViewModel.alpha
+                        bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
+                    }
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
index 9872d97..52d87d3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
@@ -42,9 +42,9 @@
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
                     viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.progress = burnInOffsets.burnInProgress
-                        view.translationX = burnInOffsets.burnInXOffset.toFloat()
-                        view.translationY = burnInOffsets.burnInYOffset.toFloat()
+                        view.progress = burnInOffsets.progress
+                        view.translationX = burnInOffsets.x.toFloat()
+                        view.translationY = burnInOffsets.y.toFloat()
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
index bab04f2..d4621e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
@@ -59,8 +59,8 @@
 
                 launch {
                     viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.translationX = burnInOffsets.burnInXOffset.toFloat()
-                        view.translationY = burnInOffsets.burnInYOffset.toFloat()
+                        view.translationX = burnInOffsets.x.toFloat()
+                        view.translationY = burnInOffsets.y.toFloat()
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransition.kt
new file mode 100644
index 0000000..b58a80f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransition.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.keyguard.ui.transitions
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Each DeviceEntryIconTransition is responsible for updating the given parameters for the current
+ * keyguard transition.
+ * *
+ * MUST list implementing classes in dagger module [DeviceEntryIconTransitionModule].
+ */
+interface DeviceEntryIconTransition {
+    val deviceEntryParentViewAlpha: Flow<Float>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
new file mode 100644
index 0000000..9d557bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.keyguard.ui.transitions
+
+import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+@Module
+abstract class DeviceEntryIconTransitionModule {
+    @Binds
+    @IntoSet
+    abstract fun aodToLockscreen(
+        impl: AodToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun aodToGone(impl: AodToGoneTransitionViewModel): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun dozingToLockscreen(
+        impl: DozingToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun dreamingToLockscreen(
+        impl: DreamingToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenToAod(
+        impl: LockscreenToAodTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenToDreaming(
+        impl: LockscreenToDreamingTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenToOccluded(
+        impl: LockscreenToOccludedTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenToPrimaryBouncer(
+        impl: LockscreenToPrimaryBouncerTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenToGone(
+        impl: LockscreenToGoneTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun goneToAod(impl: GoneToAodTransitionViewModel): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun occludedToAod(impl: OccludedToAodTransitionViewModel): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun occludedToLockscreen(
+        impl: OccludedToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun primaryBouncerToAod(
+        impl: PrimaryBouncerToAodTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun primaryBouncerToLockscreen(
+        impl: PrimaryBouncerToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index c9e3954..af1d0df 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -40,7 +40,10 @@
     attrs: AttributeSet?,
     defStyleAttrs: Int = 0,
 ) : FrameLayout(context, attrs, defStyleAttrs) {
-    val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs)
+    val longPressHandlingView: LongPressHandlingView =
+        LongPressHandlingView(context, attrs) {
+            context.resources.getInteger(R.integer.config_lockIconLongPress).toLong()
+        }
     val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg }
     val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg }
     val aodFpDrawable: LottieDrawable = LottieDrawable()
@@ -105,7 +108,7 @@
         // FINGERPRINT
         animatedIconDrawable.addState(
             getIconState(IconType.FINGERPRINT, false),
-            context.getDrawable(R.drawable.ic_kg_fingerprint)!!,
+            context.getDrawable(R.drawable.ic_fingerprint)!!,
             R.id.locked_fp,
         )
 
@@ -220,7 +223,7 @@
         val lp = longPressHandlingView.layoutParams as LayoutParams
         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
-        longPressHandlingView.setLayoutParams(lp)
+        longPressHandlingView.layoutParams = lp
     }
 
     private fun addIconImageView() {
@@ -231,7 +234,7 @@
         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
         lp.gravity = Gravity.CENTER
-        iconView.setLayoutParams(lp)
+        iconView.layoutParams = lp
     }
 
     private fun addBgImageView() {
@@ -240,7 +243,7 @@
         val lp = bgView.layoutParams as LayoutParams
         lp.height = ViewGroup.LayoutParams.MATCH_PARENT
         lp.width = ViewGroup.LayoutParams.MATCH_PARENT
-        bgView.setLayoutParams(lp)
+        bgView.layoutParams = lp
     }
 
     fun getIconState(icon: IconType, aod: Boolean): IntArray {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 975d62a..68e16ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -34,9 +34,9 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
-import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -49,8 +49,8 @@
     private val context: Context,
     private val configurationState: ConfigurationState,
     private val configurationController: ConfigurationController,
-    private val dozeParameters: DozeParameters,
     private val featureFlags: FeatureFlagsClassic,
+    private val iconBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val notificationIconAreaController: NotificationIconAreaController,
@@ -94,6 +94,7 @@
                     nicAodViewModel,
                     configurationState,
                     configurationController,
+                    iconBindingFailureTracker,
                     nicAodIconViewStore,
                 )
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
index ace970a..13ea8ff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
@@ -36,6 +36,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
@@ -56,12 +58,15 @@
     private val featureFlags: FeatureFlags,
     private val lockIconViewController: Lazy<LockIconViewController>,
     private val deviceEntryIconViewModel: Lazy<DeviceEntryIconViewModel>,
+    private val deviceEntryForegroundViewModel: Lazy<DeviceEntryForegroundViewModel>,
+    private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>,
     private val falsingManager: Lazy<FalsingManager>,
 ) : KeyguardSection() {
     private val deviceEntryIconViewId = R.id.device_entry_icon_view
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!keyguardBottomAreaRefactor() &&
+        if (
+            !keyguardBottomAreaRefactor() &&
                 !featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)
         ) {
             return
@@ -87,6 +92,8 @@
                 DeviceEntryIconViewBinder.bind(
                     it,
                     deviceEntryIconViewModel.get(),
+                    deviceEntryForegroundViewModel.get(),
+                    deviceEntryBackgroundViewModel.get(),
                     falsingManager.get(),
                 )
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 078feff..c2aedca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 00966f2..ea2bdf7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
@@ -40,6 +41,7 @@
     private val sharedNotificationContainer: SharedNotificationContainer,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
     private var disposableHandle: DisposableHandle? = null
@@ -69,6 +71,7 @@
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
                 controller,
+                notificationStackSizeCalculator,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index bf95c77..dc2ad8d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..4d2af0c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Breaks down AOD->GONE transition into discrete steps for corresponding views to consume. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AodToGoneTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+) : DeviceEntryIconTransition {
+
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromAodTransitionInteractor.TO_GONE_DURATION,
+            transitionFlow = interactor.transition(KeyguardState.AOD, KeyguardState.GONE),
+        )
+
+    override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index 024707a..14de01b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -17,22 +17,29 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
 
 /**
  * Breaks down AOD->LOCKSCREEN transition into discrete steps for corresponding views to consume.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class AodToLockscreenTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
-) {
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         KeyguardTransitionAnimationFlow(
@@ -47,4 +54,21 @@
             onStart = { 1f },
             onStep = { 1f },
         )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps ->
+            if (isUdfps) {
+                // fade in
+                transitionAnimation.createFlow(
+                    duration = 250.milliseconds,
+                    onStep = { it },
+                    onFinish = { 1f },
+                )
+            } else {
+                // background view isn't visible, so return an empty flow
+                emptyFlow()
+            }
+        }
+
+    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
new file mode 100644
index 0000000..06661d0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+
+/** Breaks down AOD->OCCLUDED transition into discrete steps for corresponding views to consume. */
+@SysUISingleton
+class AodToOccludedTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
+            transitionFlow = interactor.transition(KeyguardState.AOD, KeyguardState.OCCLUDED),
+        )
+
+    override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
new file mode 100644
index 0000000..3e8bbb3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.content.Context
+import com.android.settingslib.Utils
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+
+/** Models the UI state for the device entry icon background view. */
+@ExperimentalCoroutinesApi
+class DeviceEntryBackgroundViewModel
+@Inject
+constructor(
+    val context: Context,
+    configurationRepository: ConfigurationRepository, // TODO (b/309655554): create & use interactor
+    lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
+    aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
+    primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
+    occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
+    occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+) {
+    private val color: Flow<Int> =
+        configurationRepository.onAnyConfigurationChange
+            .map {
+                Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface)
+            }
+            .onStart {
+                emit(
+                    Utils.getColorAttrDefaultColor(
+                        context,
+                        com.android.internal.R.attr.colorSurface
+                    )
+                )
+            }
+    private val alpha: Flow<Float> =
+        setOf(
+                lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+            )
+            .merge()
+
+    val viewModel: Flow<BackgroundViewModel> =
+        combine(color, alpha) { color, alpha ->
+            BackgroundViewModel(
+                alpha = alpha,
+                tint = color,
+            )
+        }
+
+    data class BackgroundViewModel(
+        val alpha: Float,
+        val tint: Int,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
new file mode 100644
index 0000000..99529a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.content.Context
+import com.android.settingslib.Utils
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Models the UI state for the device entry icon foreground view (displayed icon). */
+@ExperimentalCoroutinesApi
+class DeviceEntryForegroundViewModel
+@Inject
+constructor(
+    val context: Context,
+    configurationRepository: ConfigurationRepository, // TODO (b/309655554): create & use interactor
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    transitionInteractor: KeyguardTransitionInteractor,
+    deviceEntryIconViewModel: DeviceEntryIconViewModel,
+) {
+    private val isShowingAod: Flow<Boolean> =
+        transitionInteractor.startedKeyguardState.map { keyguardState ->
+            keyguardState == KeyguardState.AOD
+        }
+    private val color: Flow<Int> =
+        configurationRepository.onAnyConfigurationChange
+            .map { Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary) }
+            .onStart {
+                emit(Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary))
+            }
+    private val useAodIconVariant: Flow<Boolean> =
+        combine(isShowingAod, deviceEntryUdfpsInteractor.isUdfpsSupported) {
+                isTransitionToAod,
+                isUdfps ->
+                isTransitionToAod && isUdfps
+            }
+            .distinctUntilChanged()
+    private val padding: Flow<Int> =
+        configurationRepository.scaleForResolution.map { scale ->
+            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
+                .roundToInt()
+        }
+
+    val viewModel: Flow<ForegroundIconViewModel> =
+        combine(
+            deviceEntryIconViewModel.iconType,
+            useAodIconVariant,
+            color,
+            padding,
+        ) { iconType, useAodVariant, color, padding ->
+            ForegroundIconViewModel(
+                type = iconType,
+                useAodVariant = useAodVariant,
+                tint = color,
+                padding = padding,
+            )
+        }
+
+    data class ForegroundIconViewModel(
+        val type: DeviceEntryIconView.IconType,
+        val useAodVariant: Boolean,
+        val tint: Int,
+        val padding: Int,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 842dde3..5b5a103 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -12,57 +12,202 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import android.graphics.Color
+import android.animation.FloatEvaluator
+import android.animation.IntEvaluator
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
 
+/** Models the UI state for the containing device entry icon & long-press handling view. */
 @ExperimentalCoroutinesApi
-class DeviceEntryIconViewModel @Inject constructor() {
-    // TODO: b/305234447 update these states from the data layer
-    val iconViewModel: Flow<IconViewModel> =
-        flowOf(
-            IconViewModel(
-                type = DeviceEntryIconView.IconType.LOCK,
-                useAodVariant = false,
-                tint = Color.WHITE,
-                alpha = 1f,
-                padding = 48,
+class DeviceEntryIconViewModel
+@Inject
+constructor(
+    transitions: Set<@JvmSuppressWildcards DeviceEntryIconTransition>,
+    burnInInteractor: BurnInInteractor,
+    shadeInteractor: ShadeInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    transitionInteractor: KeyguardTransitionInteractor,
+    val keyguardInteractor: KeyguardInteractor,
+    val viewModel: AodToLockscreenTransitionViewModel,
+    val shadeDependentFlows: ShadeDependentFlows,
+    private val sceneContainerFlags: SceneContainerFlags,
+    private val keyguardViewController: Lazy<KeyguardViewController>,
+    private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor,
+    udfpsInteractor: DeviceEntryUdfpsInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+) {
+    private val intEvaluator = IntEvaluator()
+    private val floatEvaluator = FloatEvaluator()
+    private val toAodFromState: Flow<KeyguardState> =
+        transitionInteractor.transitionStepsToState(KeyguardState.AOD).map { it.from }
+    private val showingAlternateBouncer: Flow<Boolean> =
+        transitionInteractor.startedKeyguardState.map { keyguardState ->
+            keyguardState == KeyguardState.ALTERNATE_BOUNCER
+        }
+    private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) }
+    private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) }
+    private val transitionAlpha: Flow<Float> =
+        transitions.map { it.deviceEntryParentViewAlpha }.merge()
+    private val alphaMultiplierFromShadeExpansion: Flow<Float> =
+        combine(
+            showingAlternateBouncer,
+            shadeExpansion,
+            qsProgress,
+        ) { showingAltBouncer, shadeExpansion, qsProgress ->
+            val interpolatedQsProgress = (qsProgress * 2).coerceIn(0f, 1f)
+            if (showingAltBouncer) {
+                1f
+            } else {
+                (1f - shadeExpansion) * (1f - interpolatedQsProgress)
+            }
+        }
+    // Burn-in offsets in AOD
+    private val nonAnimatedBurnInOffsets: Flow<BurnInOffsets> =
+        combine(
+            burnInInteractor.deviceEntryIconXOffset,
+            burnInInteractor.deviceEntryIconYOffset,
+            burnInInteractor.udfpsProgress
+        ) { fullyDozingBurnInX, fullyDozingBurnInY, fullyDozingBurnInProgress ->
+            BurnInOffsets(
+                fullyDozingBurnInX,
+                fullyDozingBurnInY,
+                fullyDozingBurnInProgress,
             )
-        )
-    val backgroundViewModel: Flow<BackgroundViewModel> =
-        flowOf(BackgroundViewModel(alpha = 1f, tint = Color.GRAY))
-    val burnInViewModel: Flow<BurnInViewModel> = flowOf(BurnInViewModel(0, 0, 0f))
-    val isLongPressEnabled: Flow<Boolean> = flowOf(true)
+        }
+    // Burn-in offsets that animate based on the transition amount to AOD
+    private val animatedBurnInOffsets: Flow<BurnInOffsets> =
+        combine(
+            nonAnimatedBurnInOffsets,
+            transitionInteractor.transitionStepsToState(KeyguardState.AOD)
+        ) { burnInOffsets, transitionStepsToAod ->
+            val dozeAmount = transitionStepsToAod.value
+            BurnInOffsets(
+                intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.x),
+                intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.y),
+                floatEvaluator.evaluate(dozeAmount, 0, burnInOffsets.progress)
+            )
+        }
+
+    val deviceEntryViewAlpha: Flow<Float> =
+        combine(
+            transitionAlpha,
+            alphaMultiplierFromShadeExpansion,
+        ) { alpha, alphaMultiplier ->
+            alpha * alphaMultiplier
+        }
+    val useBackgroundProtection: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
+    val burnInOffsets: Flow<BurnInOffsets> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolled ->
+            if (udfpsEnrolled) {
+                toAodFromState.flatMapLatest { fromState ->
+                    when (fromState) {
+                        KeyguardState.AOD,
+                        KeyguardState.GONE,
+                        KeyguardState.OCCLUDED,
+                        KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+                        KeyguardState.OFF,
+                        KeyguardState.DOZING,
+                        KeyguardState.DREAMING,
+                        KeyguardState.PRIMARY_BOUNCER -> nonAnimatedBurnInOffsets
+                        KeyguardState.ALTERNATE_BOUNCER -> animatedBurnInOffsets
+                        KeyguardState.LOCKSCREEN ->
+                            shadeDependentFlows.transitionFlow(
+                                flowWhenShadeIsExpanded = nonAnimatedBurnInOffsets,
+                                flowWhenShadeIsNotExpanded = animatedBurnInOffsets,
+                            )
+                    }
+                }
+            } else {
+                // If UDFPS isn't enrolled, we don't show any UI on AOD so there's no need
+                // to use burn in offsets at all
+                flowOf(BurnInOffsets(x = 0, y = 0, progress = 0f))
+            }
+        }
+    val iconType: Flow<DeviceEntryIconView.IconType> =
+        combine(
+            udfpsInteractor.isListeningForUdfps,
+            deviceEntryInteractor.isUnlocked,
+        ) { isListeningForUdfps, isUnlocked ->
+            if (isUnlocked) {
+                DeviceEntryIconView.IconType.UNLOCK
+            } else {
+                if (isListeningForUdfps) {
+                    DeviceEntryIconView.IconType.FINGERPRINT
+                } else {
+                    DeviceEntryIconView.IconType.LOCK
+                }
+            }
+        }
+    val isLongPressEnabled: Flow<Boolean> =
+        combine(
+            iconType,
+            deviceEntryUdfpsInteractor.isUdfpsSupported,
+        ) { deviceEntryStatus, isUdfps ->
+            when (deviceEntryStatus) {
+                DeviceEntryIconView.IconType.LOCK -> isUdfps
+                DeviceEntryIconView.IconType.UNLOCK -> true
+                DeviceEntryIconView.IconType.FINGERPRINT -> false
+            }
+        }
     val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> =
-        flowOf(DeviceEntryIconView.AccessibilityHintType.NONE)
+        combine(iconType, isLongPressEnabled) { deviceEntryStatus, longPressEnabled ->
+            if (longPressEnabled) {
+                deviceEntryStatus.toAccessibilityHintType()
+            } else {
+                DeviceEntryIconView.AccessibilityHintType.NONE
+            }
+        }
 
     fun onLongPress() {
-        // TODO() vibrate & perform action based on current lock/unlock state
+        deviceEntryHapticsInteractor.vibrateSuccess()
+
+        // TODO (b/309804148): play auth ripple via an interactor
+
+        if (sceneContainerFlags.isEnabled()) {
+            deviceEntryInteractor.attemptDeviceEntry()
+        } else {
+            keyguardViewController.get().showPrimaryBouncer(/* scrim */ true)
+        }
     }
-    data class BurnInViewModel(
-        val x: Int, // current x burn in offset based on the aodTransitionAmount
-        val y: Int, // current y burn in offset based on the aodTransitionAmount
-        val progress: Float, // current progress based on the aodTransitionAmount
-    )
 
-    class IconViewModel(
-        val type: DeviceEntryIconView.IconType,
-        val useAodVariant: Boolean,
-        val tint: Int,
-        val alpha: Float,
-        val padding: Int,
-    )
-
-    class BackgroundViewModel(
-        val alpha: Float,
-        val tint: Int,
-    )
+    private fun DeviceEntryIconView.IconType.toAccessibilityHintType():
+        DeviceEntryIconView.AccessibilityHintType {
+        return when (this) {
+            DeviceEntryIconView.IconType.LOCK ->
+                DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE
+            DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER
+            DeviceEntryIconView.IconType.FINGERPRINT ->
+                DeviceEntryIconView.AccessibilityHintType.NONE
+        }
+    }
 }
+
+data class BurnInOffsets(
+    val x: Int, // current x burn in offset based on the aodTransitionAmount
+    val y: Int, // current y burn in offset based on the aodTransitionAmount
+    val progress: Float, // current progress based on the aodTransitionAmount
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..27fb8a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDozingTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down DOZING->LOCKSCREEN transition into discrete steps for corresponding views to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class DozingToLockscreenTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation: KeyguardTransitionAnimationFlow =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromDozingTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            transitionFlow = interactor.dozingToLockscreenTransition,
+        )
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index e24d326..a3b8b85 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -18,27 +18,34 @@
 
 import com.android.app.animation.Interpolators.EMPHASIZED
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
 
 /**
  * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
  * consume.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class DreamingToLockscreenTransitionViewModel
 @Inject
 constructor(
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    private val fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
-) {
+    private val fromDreamingTransitionInteractor: FromDreamingTransitionInteractor,
+    private val deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
     fun startTransition() = fromDreamingTransitionInteractor.startToLockscreenTransition()
 
     private val transitionAnimation =
@@ -88,4 +95,15 @@
             duration = 250.milliseconds,
             onStep = { it },
         )
+
+    val deviceEntryBackgroundViewAlpha =
+        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps ->
+            if (isUdfps) {
+                // immediately show; will fade in with deviceEntryParentViewAlpha
+                transitionAnimation.immediatelyTransitionTo(1f)
+            } else {
+                emptyFlow()
+            }
+        }
+    override val deviceEntryParentViewAlpha = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index 601dbcc..62b2281 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -18,20 +18,27 @@
 
 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
 
 /** Breaks down GONE->AOD transition into discrete steps for corresponding views to consume. */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class GoneToAodTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
-) {
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         KeyguardTransitionAnimationFlow(
@@ -60,4 +67,21 @@
             onStart = { 0f },
             onStep = { it },
         )
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolled ->
+            if (udfpsEnrolled) {
+                // fade in at the end of the transition to give time for FP to start running
+                // and avoid a flicker of the unlocked icon
+                transitionAnimation.createFlow(
+                    startTime = 1100.milliseconds,
+                    duration = 200.milliseconds,
+                    onStep = { it },
+                    onFinish = { 1f },
+                )
+            } else {
+                emptyFlow()
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
new file mode 100644
index 0000000..2bf12e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Breaks down LOCKSCREEN->AOD transition into discrete steps for corresponding views to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class LockscreenToAodTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition {
+
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromLockscreenTransitionInteractor.TO_AOD_DURATION,
+            transitionFlow = interactor.lockscreenToAodTransition,
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.createFlow(
+                    duration = 300.milliseconds,
+                    onStep = { 1 - it },
+                    onFinish = { 0f },
+                ),
+        )
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest {
+            isUdfpsEnrolledAndEnabled ->
+            if (isUdfpsEnrolledAndEnabled) {
+                shadeDependentFlows.transitionFlow(
+                    flowWhenShadeIsExpanded = // fade in
+                    transitionAnimation.createFlow(
+                            duration = 300.milliseconds,
+                            onStep = { it },
+                            onFinish = { 1f },
+                        ),
+                    flowWhenShadeIsNotExpanded = transitionAnimation.immediatelyTransitionTo(1f),
+                )
+            } else {
+                shadeDependentFlows.transitionFlow(
+                    flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
+                    flowWhenShadeIsNotExpanded = // fade out
+                    transitionAnimation.createFlow(
+                            duration = 200.milliseconds,
+                            onStep = { 1f - it },
+                            onFinish = { 0f },
+                        ),
+                )
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index a3ae67d..5229613 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
@@ -34,7 +35,8 @@
 @Inject
 constructor(
     interactor: KeyguardTransitionInteractor,
-) {
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition {
     private val transitionAnimation =
         KeyguardTransitionAnimationFlow(
             transitionDuration = TO_DREAMING_DURATION,
@@ -60,6 +62,12 @@
             onStep = { 1f - it },
         )
 
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded = lockscreenAlpha,
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
+        )
+
     companion object {
         @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..59e5aa8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->GONE transition into discrete steps for corresponding views to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class LockscreenToGoneTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+) : DeviceEntryIconTransition {
+
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
+            transitionFlow = interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
+        )
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index d3ea89c..d49bc49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
@@ -33,8 +34,9 @@
 class LockscreenToOccludedTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
-) {
+    interactor: KeyguardTransitionInteractor,
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition {
     private val transitionAnimation =
         KeyguardTransitionAnimationFlow(
             transitionDuration = TO_OCCLUDED_DURATION,
@@ -59,4 +61,10 @@
             interpolator = EMPHASIZED_ACCELERATE,
         )
     }
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded = lockscreenAlpha,
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..f04b67a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class LockscreenToPrimaryBouncerTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            transitionFlow =
+                interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER),
+        )
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.createFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onFinish = { 0f }
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f)
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
new file mode 100644
index 0000000..f7cff9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/** Breaks down OCCLUDED->AOD transition into discrete steps for corresponding views to consume. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class OccludedToAodTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromOccludedTransitionInteractor.TO_AOD_DURATION,
+            transitionFlow = interactor.transition(KeyguardState.OCCLUDED, KeyguardState.AOD),
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled
+            ->
+            if (udfpsEnrolledAndEnabled) {
+                transitionAnimation.immediatelyTransitionTo(1f)
+            } else {
+                emptyFlow()
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index 6845c55..0bdc85d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -18,23 +18,30 @@
 
 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
 
 /**
  * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
  * consume.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class OccludedToLockscreenTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
-) {
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor
+) : DeviceEntryIconTransition {
     private val transitionAnimation =
         KeyguardTransitionAnimationFlow(
             transitionDuration = TO_LOCKSCREEN_DURATION,
@@ -58,4 +65,16 @@
             duration = 250.milliseconds,
             onStep = { it },
         )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest {
+            isUdfpsEnrolledAndEnabled ->
+            if (isUdfpsEnrolledAndEnabled) {
+                transitionAnimation.immediatelyTransitionTo(1f)
+            } else {
+                emptyFlow()
+            }
+        }
+
+    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
new file mode 100644
index 0000000..05a6d58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Breaks down PRIMARY BOUNCER->AOD transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class PrimaryBouncerToAodTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
+            transitionFlow =
+                interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD),
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfpsEnrolledAndEnabled ->
+            if (isUdfpsEnrolledAndEnabled) {
+                transitionAnimation.immediatelyTransitionTo(0f)
+            } else {
+                emptyFlow()
+            }
+        }
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest {
+            isUdfpsEnrolledAndEnabled ->
+            if (isUdfpsEnrolledAndEnabled) {
+                transitionAnimation.createFlow(
+                    duration = 300.milliseconds,
+                    onStep = { it },
+                    onFinish = { 1f },
+                )
+            } else {
+                emptyFlow()
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..3cf793a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class PrimaryBouncerToLockscreenTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            transitionFlow =
+                interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.LOCKSCREEN),
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps ->
+            if (isUdfps) {
+                transitionAnimation.immediatelyTransitionTo(1f)
+            } else {
+                emptyFlow()
+            }
+        }
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt
new file mode 100644
index 0000000..e45d537
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** Helper for flows that depend on the shade expansion */
+class ShadeDependentFlows
+@Inject
+constructor(
+    transitionInteractor: KeyguardTransitionInteractor,
+    shadeInteractor: ShadeInteractor,
+) {
+    /** When the last keyguard state transition started, was the shade fully expanded? */
+    private val lastStartedTransitionHadShadeFullyExpanded: Flow<Boolean> =
+        transitionInteractor.startedKeyguardState.sample(shadeInteractor.isAnyFullyExpanded)
+
+    /**
+     * Decide which flow to use depending on the shade expansion state at the start of the last
+     * keyguard state transition.
+     */
+    fun <T> transitionFlow(
+        flowWhenShadeIsExpanded: Flow<T>,
+        flowWhenShadeIsNotExpanded: Flow<T>,
+    ): Flow<T> {
+        val filteredFlowWhenShadeIsExpanded =
+            flowWhenShadeIsExpanded
+                .sample(lastStartedTransitionHadShadeFullyExpanded, ::Pair)
+                .filter { (_, shadeFullyExpanded) -> shadeFullyExpanded }
+                .map { (valueWhenShadeIsExpanded, _) -> valueWhenShadeIsExpanded }
+        val filteredFlowWhenShadeIsNotExpanded =
+            flowWhenShadeIsNotExpanded
+                .sample(lastStartedTransitionHadShadeFullyExpanded, ::Pair)
+                .filter { (_, shadeFullyExpanded) -> !shadeFullyExpanded }
+                .map { (valueWhenShadeIsNotExpanded, _) -> valueWhenShadeIsNotExpanded }
+        return merge(filteredFlowWhenShadeIsExpanded, filteredFlowWhenShadeIsNotExpanded)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
index c10a463..6e77e13e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.Context
-import com.android.systemui.res.R
-import com.android.systemui.keyguard.domain.interactor.BurnInOffsets
+import com.android.systemui.keyguard.domain.interactor.Offsets
 import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -35,7 +35,7 @@
     val context: Context,
 ) {
     val alpha: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets
+    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
     val isVisible: Flow<Boolean> = alpha.map { it != 0f }
 
     // Padding between the fingerprint icon and its bounding box in pixels.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
index 0b1079f..642904d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
@@ -19,9 +19,9 @@
 import android.content.Context
 import androidx.annotation.ColorInt
 import com.android.settingslib.Utils.getColorAttrDefaultColor
-import com.android.systemui.keyguard.domain.interactor.BurnInOffsets
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.Offsets
 import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -185,7 +185,7 @@
         keyguardInteractor,
     ) {
     val dozeAmount: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets
+    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
 
     // Padding between the fingerprint icon and its bounding box in pixels.
     val padding: Flow<Int> =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index 2034d97..dcbf670 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -37,8 +37,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaDeviceData
 import com.android.systemui.media.controls.util.MediaControllerFactory
@@ -70,7 +68,6 @@
     @Main private val fgExecutor: Executor,
     @Background private val bgExecutor: Executor,
     dumpManager: DumpManager,
-    private val featureFlags: FeatureFlagsClassic,
 ) : MediaDataManager.Listener, Dumpable {
 
     private val listeners: MutableSet<Listener> = mutableSetOf()
@@ -392,13 +389,6 @@
                 )
             }
 
-            if (!featureFlags.isEnabled(Flags.MEDIA_DEVICE_NAME_FIX)) {
-                if (controller == null || routingSession != null) {
-                    return routingSession?.name?.toString() ?: device?.name
-                }
-                return null
-            }
-
             if (controller == null) {
                 // In resume state, we don't have a controller - just use the device name
                 return device?.name
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 1ec43c5..d277f32 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -60,7 +60,7 @@
     }
 
     companion object {
-        @JvmField val GUTS_ANIMATION_DURATION = 500L
+        @JvmField val GUTS_ANIMATION_DURATION = 234L
     }
 
     /** A listener when the current dimensions of the player change */
@@ -234,7 +234,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = false
+            applyImmediately = false,
+            isGutsAnimation = true,
         )
     }
 
@@ -254,7 +255,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = immediate
+            applyImmediately = immediate,
+            isGutsAnimation = true,
         )
     }
 
@@ -414,7 +416,10 @@
      * it's not available, it will recreate one by measuring, which may be expensive.
      */
     @VisibleForTesting
-    fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+    fun obtainViewState(
+        state: MediaHostState?,
+        isGutsAnimation: Boolean = false
+    ): TransitionViewState? {
         if (state == null || state.measurementInput == null) {
             return null
         }
@@ -423,7 +428,7 @@
         val viewState = viewStates[cacheKey]
         if (viewState != null) {
             // we already have cached this measurement, let's continue
-            if (state.squishFraction <= 1f) {
+            if (state.squishFraction <= 1f && !isGutsAnimation) {
                 return squishViewState(viewState, state.squishFraction)
             }
             return viewState
@@ -455,13 +460,14 @@
 
             // Given that we have a measurement and a view, let's get (guaranteed) viewstates
             // from the start and end state and interpolate them
-            val startViewState = obtainViewState(startState) as TransitionViewState
+            val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState
             val endState = state.copy().also { it.expansion = 1.0f }
-            val endViewState = obtainViewState(endState) as TransitionViewState
+            val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState
             result =
                 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
         }
-        if (state.squishFraction <= 1f) {
+        // Skip the adjustments of squish view state if UMO changes due to guts animation.
+        if (state.squishFraction <= 1f && !isGutsAnimation) {
             return squishViewState(result, state.squishFraction)
         }
         return result
@@ -521,7 +527,8 @@
         @MediaLocation startLocation: Int,
         @MediaLocation endLocation: Int,
         transitionProgress: Float,
-        applyImmediately: Boolean
+        applyImmediately: Boolean,
+        isGutsAnimation: Boolean = false,
     ) =
         traceSection("MediaViewController#setCurrentState") {
             currentEndLocation = endLocation
@@ -537,7 +544,7 @@
             // Obtain the view state that we'd want to be at the end
             // The view might not be bound yet or has never been measured and in that case will be
             // reset once the state is fully available
-            var endViewState = obtainViewState(endHostState) ?: return
+            var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return
             endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!!
             layoutController.setMeasureState(endViewState)
 
@@ -548,7 +555,7 @@
             }
 
             val result: TransitionViewState
-            var startViewState = obtainViewState(startHostState)
+            var startViewState = obtainViewState(startHostState, isGutsAnimation)
             startViewState = updateViewStateSize(startViewState, startLocation, tmpState3)
 
             if (!endHostState.visible) {
@@ -602,7 +609,8 @@
                 applyImmediately,
                 shouldAnimate,
                 animationDuration,
-                animationDelay
+                animationDelay,
+                isGutsAnimation,
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
index 736f7cf..ae554d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
 import com.android.systemui.qs.tiles.base.logging.QSTileLogger
 import com.android.systemui.qs.tiles.impl.di.QSTileComponent
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.user.data.repository.UserRepository
@@ -60,12 +61,17 @@
     ) : QSTileViewModelFactory<T> {
 
         /**
-         * Creates [QSTileViewModelImpl] based on the interactors obtained from [component].
-         * Reference of that [component] is then stored along the view model.
+         * Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent].
+         * Reference of that [QSTileComponent] is then stored along the view model.
          */
-        fun create(tileSpec: TileSpec, component: QSTileComponent<T>): QSTileViewModelImpl<T> =
-            QSTileViewModelImpl(
-                qsTileConfigProvider.getConfig(tileSpec.spec),
+        fun create(
+            tileSpec: TileSpec,
+            componentFactory: (config: QSTileConfig) -> QSTileComponent<T>
+        ): QSTileViewModelImpl<T> {
+            val config = qsTileConfigProvider.getConfig(tileSpec.spec)
+            val component = componentFactory(config)
+            return QSTileViewModelImpl(
+                config,
                 component::userActionInteractor,
                 component::dataInteractor,
                 component::dataToStateMapper,
@@ -77,6 +83,7 @@
                 systemClock,
                 backgroundDispatcher,
             )
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 12a083e..5e19439 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -116,7 +116,7 @@
             )
 
     override fun forceUpdate() {
-        forceUpdates.tryEmit(Unit)
+        tileScope.launch { forceUpdates.emit(Unit) }
     }
 
     override fun onUserChanged(user: UserHandle) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
index 7d7af64..27007bb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
@@ -19,6 +19,11 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.qs.QSFactory
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
+import com.android.systemui.qs.tiles.impl.custom.di.QSTileConfigModule
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter
@@ -34,18 +39,31 @@
     private val adapterFactory: QSTileViewModelAdapter.Factory,
     private val tileMap:
         Map<String, @JvmSuppressWildcards Provider<@JvmSuppressWildcards QSTileViewModel>>,
+    private val customTileComponentBuilder: CustomTileComponent.Builder,
+    private val customTileViewModelFactory: QSTileViewModelFactory.Component<CustomTileDataModel>,
 ) : QSFactory {
 
     init {
         for (viewModelTileSpec in tileMap.keys) {
-            // throws an exception when there is no config for a tileSpec of an injected viewModel
-            qsTileConfigProvider.getConfig(viewModelTileSpec)
+            require(qsTileConfigProvider.hasConfig(viewModelTileSpec)) {
+                "No config for $viewModelTileSpec"
+            }
         }
     }
 
-    override fun createTile(tileSpec: String): QSTile? =
-        tileMap[tileSpec]?.let {
-            val tile = it.get()
-            adapterFactory.create(tile)
+    override fun createTile(tileSpec: String): QSTile? {
+        val viewModel: QSTileViewModel =
+            when (val spec = TileSpec.create(tileSpec)) {
+                is TileSpec.CustomTileSpec -> createCustomTileViewModel(spec)
+                is TileSpec.PlatformTileSpec -> tileMap[tileSpec]?.get()
+                is TileSpec.Invalid -> null
+            }
+                ?: return null
+        return adapterFactory.create(viewModel)
+    }
+
+    private fun createCustomTileViewModel(spec: TileSpec.CustomTileSpec): QSTileViewModel =
+        customTileViewModelFactory.create(spec) { config ->
+            customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build()
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
index 761274e..14bf25d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
@@ -19,17 +19,18 @@
 import android.os.UserHandle
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
 @QSTileScope
-class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileData> {
+class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileDataModel> {
 
     override fun tileData(
         user: UserHandle,
         triggers: Flow<DataUpdateTrigger>
-    ): Flow<CustomTileData> {
+    ): Flow<CustomTileDataModel> {
         TODO("Not yet implemented")
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
index f7bec02..e23a5c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
@@ -17,15 +17,16 @@
 package com.android.systemui.qs.tiles.impl.custom
 
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import javax.inject.Inject
 
 @QSTileScope
-class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileData> {
+class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileDataModel> {
 
-    override fun map(config: QSTileConfig, data: CustomTileData): QSTileState {
+    override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState {
         TODO("Not yet implemented")
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
index 6c1c1a3..f34704b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
@@ -18,14 +18,15 @@
 
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import javax.inject.Inject
 
 @QSTileScope
 class CustomTileUserActionInteractor @Inject constructor() :
-    QSTileUserActionInteractor<CustomTileData> {
+    QSTileUserActionInteractor<CustomTileDataModel> {
 
-    override suspend fun handleInput(input: QSTileInput<CustomTileData>) {
+    override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) {
         TODO("Not yet implemented")
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
index 01df906..88bc8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
@@ -16,13 +16,14 @@
 
 package com.android.systemui.qs.tiles.impl.custom.di
 
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import com.android.systemui.qs.tiles.impl.di.QSTileComponent
 import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import dagger.Subcomponent
 
 @QSTileScope
 @Subcomponent(modules = [QSTileConfigModule::class, CustomTileModule::class])
-interface CustomTileComponent : QSTileComponent<Any> {
+interface CustomTileComponent : QSTileComponent<CustomTileDataModel> {
 
     @Subcomponent.Builder
     interface Builder {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
index 482bf9b..83767aa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
@@ -19,13 +19,13 @@
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.impl.custom.CustomTileData
 import com.android.systemui.qs.tiles.impl.custom.CustomTileInteractor
 import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper
 import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepositoryImpl
 import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
 import dagger.Binds
 import dagger.Module
 
@@ -36,15 +36,15 @@
     @Binds
     fun bindDataInteractor(
         dataInteractor: CustomTileInteractor
-    ): QSTileDataInteractor<CustomTileData>
+    ): QSTileDataInteractor<CustomTileDataModel>
 
     @Binds
     fun bindUserActionInteractor(
         userActionInteractor: CustomTileUserActionInteractor
-    ): QSTileUserActionInteractor<CustomTileData>
+    ): QSTileUserActionInteractor<CustomTileDataModel>
 
     @Binds
-    fun bindMapper(customTileMapper: CustomTileMapper): QSTileDataToStateMapper<CustomTileData>
+    fun bindMapper(customTileMapper: CustomTileMapper): QSTileDataToStateMapper<CustomTileDataModel>
 
     @Binds
     fun bindCustomTileDefaultsRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt
rename to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
index bb5a229..f095c01 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.tiles.impl.custom
+package com.android.systemui.qs.tiles.impl.custom.domain.entity
 
 import android.content.ComponentName
 import android.graphics.drawable.Icon
@@ -22,12 +22,11 @@
 import android.service.quicksettings.Tile
 import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
 
-data class CustomTileData(
+data class CustomTileDataModel(
     val user: UserHandle,
     val componentName: ComponentName,
     val tile: Tile,
     val callingAppUid: Int,
-    val isActive: Boolean,
     val hasPendingBind: Boolean,
     val shouldShowChevron: Boolean,
     val defaultTileLabel: CharSequence?,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
index 3f3b94e..0609e79 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
@@ -18,20 +18,31 @@
 
 import com.android.internal.util.Preconditions
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 
 interface QSTileConfigProvider {
 
     /**
-     * Returns a [QSTileConfig] for a [tileSpec] or throws [IllegalArgumentException] if there is no
-     * config for such [tileSpec].
+     * Returns a [QSTileConfig] for a [tileSpec]:
+     * - injected config for [TileSpec.PlatformTileSpec] or throws [IllegalArgumentException] if
+     *   there is none
+     * - new config for [TileSpec.CustomTileSpec].
+     * - throws [IllegalArgumentException] for [TileSpec.Invalid]
      */
     fun getConfig(tileSpec: String): QSTileConfig
+
+    fun hasConfig(tileSpec: String): Boolean
 }
 
 @SysUISingleton
-class QSTileConfigProviderImpl @Inject constructor(private val configs: Map<String, QSTileConfig>) :
-    QSTileConfigProvider {
+class QSTileConfigProviderImpl
+@Inject
+constructor(
+    private val configs: Map<String, QSTileConfig>,
+    private val qsEventLogger: QsEventLogger,
+) : QSTileConfigProvider {
 
     init {
         for (entry in configs.entries) {
@@ -44,6 +55,26 @@
         }
     }
 
+    override fun hasConfig(tileSpec: String): Boolean =
+        when (TileSpec.create(tileSpec)) {
+            is TileSpec.PlatformTileSpec -> configs.containsKey(tileSpec)
+            is TileSpec.CustomTileSpec -> true
+            is TileSpec.Invalid -> false
+        }
+
     override fun getConfig(tileSpec: String): QSTileConfig =
-        configs[tileSpec] ?: throw IllegalArgumentException("There is no config for spec=$tileSpec")
+        when (val spec = TileSpec.create(tileSpec)) {
+            is TileSpec.PlatformTileSpec -> {
+                configs[tileSpec]
+                    ?: throw IllegalArgumentException("There is no config for spec=$tileSpec")
+            }
+            is TileSpec.CustomTileSpec ->
+                QSTileConfig(
+                    spec,
+                    QSTileUIConfig.Empty,
+                    qsEventLogger.getNewInstanceId(),
+                )
+            is TileSpec.Invalid ->
+                throw IllegalArgumentException("TileSpec.Invalid doesn't support configs")
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 285cb5a..e9c930a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1483,16 +1483,16 @@
     }
 
     private void updateMaxDisplayedNotifications(boolean recompute) {
+        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
+            return;
+        }
+
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
         } else {
             if (SPEW_LOGCAT) Log.d(TAG, "Skipping computeMaxKeyguardNotifications() by request");
         }
 
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
-            return;
-        }
-
         if (isKeyguardShowing() && !mKeyguardBypassController.getBypassEnabled()) {
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(
                     mMaxAllowedKeyguardNotifications);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index e2e4556..8bab669 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -65,6 +65,10 @@
      */
     @Deprecated("Use ShadeInteractor instead") val legacyShadeTracking: StateFlow<Boolean>
 
+    /** Specifically tracks the user expanding the shade on the lockscreen only */
+    @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead")
+    val legacyLockscreenShadeTracking: MutableStateFlow<Boolean>
+
     /**
      * QuickSettingsController.mTracking as a flow. "Tracking" means that the user is moving quick
      * settings up or down with a pointer. Going forward, this concept will be replaced by checks
@@ -106,6 +110,9 @@
     /** Sets whether the user is moving the shade with a pointer */
     fun setLegacyShadeTracking(tracking: Boolean)
 
+    /** Sets whether the user is moving the shade with a pointer, on lockscreen only */
+    fun setLegacyLockscreenShadeTracking(tracking: Boolean)
+
     /** Amount shade has expanded with regard to the UDFPS location */
     val udfpsTransitionToFullShadeProgress: StateFlow<Float>
 
@@ -177,6 +184,8 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyShadeTracking: StateFlow<Boolean> = _legacyShadeTracking.asStateFlow()
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     private val _legacyQsTracking = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead")
     override val legacyQsTracking: StateFlow<Boolean> = _legacyQsTracking.asStateFlow()
@@ -212,6 +221,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     override fun setQsExpansion(qsExpansion: Float) {
         _qsExpansion.value = qsExpansion
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index b2ffeb3..d687ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -72,7 +72,7 @@
     userSetupRepository: UserSetupRepository,
     userSwitcherInteractor: UserSwitcherInteractor,
     sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
-    repository: ShadeRepository,
+    private val repository: ShadeRepository,
 ) {
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean> =
@@ -185,7 +185,15 @@
         if (sceneContainerFlags.isEnabled()) {
             sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade)
         } else {
-            userInteractingFlow(repository.legacyShadeTracking, repository.legacyShadeExpansion)
+            combine(
+                userInteractingFlow(
+                    repository.legacyShadeTracking,
+                    repository.legacyShadeExpansion
+                ),
+                repository.legacyLockscreenShadeTracking
+            ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
+                legacyShadeTracking || legacyLockscreenShadeTracking
+            }
         }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
index ab0d6e3..922560f 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -17,11 +17,10 @@
 package com.android.systemui.smartspace.config
 
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 
 class BcSmartspaceConfigProvider(private val featureFlags: FeatureFlags) :
     BcSmartspaceConfigPlugin {
     override val isDefaultDateWeatherDisabled: Boolean
-        get() = featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)
+        get() = true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index bf722af..2e3f3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -340,6 +340,7 @@
         )
         nsslController.resetScrollPosition()
         nsslController.resetCheckSnoozeLeavebehind()
+        shadeRepository.setLegacyLockscreenShadeTracking(false)
         setDragDownAmountAnimated(0f)
     }
 
@@ -366,6 +367,7 @@
                 cancel()
             }
         }
+        shadeRepository.setLegacyLockscreenShadeTracking(true)
     }
 
     /** Do we need a falsing check currently? */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 2cd5560..ef87406 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -269,8 +269,7 @@
     fun isDateWeatherDecoupled(): Boolean {
         execution.assertIsMainThread()
 
-        return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
-                datePlugin != null && weatherPlugin != null
+        return datePlugin != null && weatherPlugin != null
     }
 
     fun isWeatherEnabled(): Boolean {
@@ -501,8 +500,8 @@
     }
 
     private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
-        if (isDateWeatherDecoupled()) {
-            return t.featureType != SmartspaceTarget.FEATURE_WEATHER
+        if (isDateWeatherDecoupled() && t.featureType == SmartspaceTarget.FEATURE_WEATHER) {
+            return false
         }
         if (!showNotifications) {
             return t.featureType == SmartspaceTarget.FEATURE_WEATHER
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index 2ea7f61..f8bc0ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -23,27 +23,37 @@
 import androidx.collection.ArrayMap
 import androidx.lifecycle.lifecycleScope
 import com.android.internal.policy.SystemBarUtils
+import com.android.internal.statusbar.StatusBarIcon
 import com.android.internal.util.ContrastColorUtil
+import com.android.systemui.CoreStartable
 import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.IconPack
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.onConfigChanged
-import com.android.systemui.util.children
+import com.android.systemui.util.asIndenting
 import com.android.systemui.util.kotlin.mapValuesNotNullTo
-import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.kotlin.stateFlow
+import com.android.systemui.util.printCollection
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
+import dagger.Binds
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
@@ -62,11 +72,18 @@
         viewModel: NotificationIconContainerShelfViewModel,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
+        failureTracker: StatusBarIconViewBindingFailureTracker,
         viewStore: ShelfNotificationIconViewStore,
     ): DisposableHandle {
         return view.repeatWhenAttached {
             lifecycleScope.launch {
-                viewModel.icons.bindIcons(view, configuration, configurationController, viewStore)
+                viewModel.icons.bindIcons(
+                    view,
+                    configuration,
+                    configurationController,
+                    notifyBindingFailures = { failureTracker.shelfFailures = it },
+                    viewStore,
+                )
             }
         }
     }
@@ -77,18 +94,20 @@
         viewModel: NotificationIconContainerStatusBarViewModel,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
+        failureTracker: StatusBarIconViewBindingFailureTracker,
         viewStore: StatusBarNotificationIconViewStore,
     ): DisposableHandle {
         val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
         return view.repeatWhenAttached {
             lifecycleScope.run {
                 launch {
-                    val iconColors =
+                    val iconColors: Flow<NotificationIconColors> =
                         viewModel.iconColors.mapNotNull { it.iconColors(view.viewBounds) }
                     viewModel.icons.bindIcons(
                         view,
                         configuration,
                         configurationController,
+                        notifyBindingFailures = { failureTracker.statusBarFailures = it },
                         viewStore,
                     ) { _, sbiv ->
                         StatusBarIconViewBinder.bindIconColors(
@@ -110,6 +129,7 @@
         viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
+        failureTracker: StatusBarIconViewBindingFailureTracker,
         viewStore: IconViewStore,
     ): DisposableHandle {
         return view.repeatWhenAttached {
@@ -119,6 +139,7 @@
                         view,
                         configuration,
                         configurationController,
+                        notifyBindingFailures = { failureTracker.aodFailures = it },
                         viewStore,
                     ) { _, sbiv ->
                         viewModel.bindAodStatusBarIconView(sbiv, configuration)
@@ -176,7 +197,7 @@
     }
 
     /**
-     * Binds [NotificationIconsViewData] to a [NotificationIconContainer]'s [children].
+     * Binds [NotificationIconsViewData] to a [NotificationIconContainer]'s children.
      *
      * [bindIcon] will be invoked to bind a child [StatusBarIconView] to an icon associated with the
      * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the
@@ -186,6 +207,7 @@
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
+        notifyBindingFailures: (Collection<String>) -> Unit,
         viewStore: IconViewStore,
         bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit = { _, _ -> },
     ): Unit = coroutineScope {
@@ -208,57 +230,59 @@
                 FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
             }
 
-        launch {
-            layoutParams.collect { params: FrameLayout.LayoutParams ->
-                for (child in view.children) {
-                    child.layoutParams = params
-                }
-            }
-        }
-
-        val iconBindings = mutableMapOf<String, Job>()
+        val failedBindings = mutableSetOf<String>()
+        val boundViewsByNotifKey = ArrayMap<String, Pair<StatusBarIconView, Job>>()
         var prevIcons = NotificationIconsViewData()
-        sample(layoutParams, ::Pair).collect {
-            (iconsData: NotificationIconsViewData, layoutParams: FrameLayout.LayoutParams),
-            ->
+        collect { iconsData: NotificationIconsViewData ->
             val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
             prevIcons = iconsData
 
-            val replacingIcons =
-                iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, v) ->
-                    viewStore.iconView(v.notifKey).statusBarIcon
+            val replacingIcons: ArrayMap<String, StatusBarIcon> =
+                iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, info) ->
+                    boundViewsByNotifKey[info.notifKey]?.first?.statusBarIcon
                 }
             view.setReplacingIcons(replacingIcons)
 
-            val childrenByNotifKey: Map<String, StatusBarIconView> =
-                view.children.filterIsInstance<StatusBarIconView>().associateByTo(ArrayMap()) {
-                    it.notification.key
-                }
-
-            iconsDiff.removed
-                .mapNotNull { key -> childrenByNotifKey[key]?.let { key to it } }
-                .forEach { (key, child) ->
-                    view.removeView(child)
-                    iconBindings.remove(key)?.cancel()
-                }
-
-            val toAdd = iconsDiff.added.map { it.notifKey to viewStore.iconView(it.notifKey) }
-            for ((i, keyAndView) in toAdd.withIndex()) {
-                val (key, sbiv) = keyAndView
-                // The view might still be transiently added if it was just removed
-                // and added again
-                view.removeTransientView(sbiv)
-                view.addView(sbiv, i, layoutParams)
-                iconBindings.remove(key)?.cancel()
-                iconBindings[key] = launch { bindIcon(key, sbiv) }
+            for (notifKey in iconsDiff.removed) {
+                failedBindings.remove(notifKey)
+                val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
+                view.removeView(child)
+                job.cancel()
             }
 
+            val toAdd: Sequence<String> =
+                iconsDiff.added.asSequence().map { it.notifKey } + failedBindings
+            for ((idx, notifKey) in toAdd.withIndex()) {
+                val sbiv = viewStore.iconView(notifKey)
+                if (sbiv == null) {
+                    failedBindings.add(notifKey)
+                    continue
+                }
+                // The view might still be transiently added if it was just removed and added again
+                view.removeTransientView(sbiv)
+                view.addView(sbiv, idx)
+                boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
+                boundViewsByNotifKey[notifKey] =
+                    Pair(
+                        sbiv,
+                        launch {
+                            launch { layoutParams.collect { sbiv.layoutParams = it } }
+                            bindIcon(notifKey, sbiv)
+                        },
+                    )
+            }
+
+            notifyBindingFailures(failedBindings)
+
             view.setChangingViewPositions(true)
+
             // Re-sort notification icons
+            val expectedChildren =
+                iconsData.visibleKeys.mapNotNull { boundViewsByNotifKey[it.notifKey]?.first }
             val childCount = view.childCount
             for (i in 0 until childCount) {
                 val actual = view.getChildAt(i)
-                val expected = viewStore.iconView(iconsData.visibleKeys[i].notifKey)
+                val expected = expectedChildren[i]
                 if (actual === expected) {
                     continue
                 }
@@ -273,47 +297,30 @@
 
     /** External storage for [StatusBarIconView] instances. */
     fun interface IconViewStore {
-        fun iconView(key: String): StatusBarIconView
+        fun iconView(key: String): StatusBarIconView?
     }
 
     @ColorInt private val DEFAULT_AOD_ICON_COLOR = Color.WHITE
 }
 
 /** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
-class ShelfNotificationIconViewStore
-@Inject
-constructor(
-    private val notifCollection: NotifCollection,
-) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView {
-        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
-        return entry.icons.shelfIcon ?: error("No shelf IconView found for key: $key")
-    }
-}
+class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
 
 /** [IconViewStore] for the always-on display. */
 class AlwaysOnDisplayNotificationIconViewStore
 @Inject
-constructor(
-    private val notifCollection: NotifCollection,
-) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView {
-        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
-        return entry.icons.aodIcon ?: error("No AOD IconView found for key: $key")
-    }
-}
+constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
 
 /** [IconViewStore] for the status bar. */
-class StatusBarNotificationIconViewStore
-@Inject
-constructor(
-    private val notifCollection: NotifCollection,
-) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView {
-        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
-        return entry.icons.statusBarIcon ?: error("No status bar IconView found for key: $key")
+class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
+
+private fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
+    IconViewStore { key ->
+        getEntry(key)?.icons?.let(block)
     }
-}
 
 private val View.viewBounds: Rect
     get() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt
new file mode 100644
index 0000000..0c114a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.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.systemui.statusbar.notification.icon.ui.viewbinder
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import dagger.Binds
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.io.PrintWriter
+import javax.inject.Inject
+
+@SysUISingleton
+class StatusBarIconViewBindingFailureTracker @Inject constructor() : CoreStartable {
+
+    var aodFailures: Collection<String> = emptyList()
+    var statusBarFailures: Collection<String> = emptyList()
+    var shelfFailures: Collection<String> = emptyList()
+
+    // TODO(b/310681665): Ideally we wouldn't need to implement CoreStartable at all, and could just
+    //  @Binds @IntoSet the Dumpable.
+    override fun start() {
+        // no-op, we're just using this as a dumpable
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        if (!NotificationIconContainerRefactor.isEnabled) return
+        pw.asIndenting().run {
+            printCollection("AOD Icon binding failures:", aodFailures)
+            printCollection("Status Bar Icon binding failures:", statusBarFailures)
+            printCollection("Shelf Icon binding failures:", shelfFailures)
+        }
+    }
+
+    @dagger.Module
+    interface Module {
+        @Binds
+        @IntoMap
+        @ClassKey(StatusBarIconViewBindingFailureTracker::class)
+        fun bindStartable(impl: StatusBarIconViewBindingFailureTracker): CoreStartable
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index 82626acc..c03a4a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -31,6 +31,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 
@@ -83,19 +84,18 @@
     /** An Icon to show "isolated" in the IconContainer. */
     val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> =
         headsUpIconInteractor.isolatedNotification
+            .combine(icons) { isolatedNotif, iconsViewData ->
+                isolatedNotif?.let {
+                    iconsViewData.visibleKeys.firstOrNull { it.notifKey == isolatedNotif }
+                }
+            }
             .pairwise(initialValue = null)
-            .sample(combine(icons, shadeInteractor.shadeExpansion, ::Pair)) {
-                (prev, isolatedNotif),
-                (iconsViewData, shadeExpansion),
-                ->
-                val iconInfo =
-                    isolatedNotif?.let {
-                        iconsViewData.visibleKeys.firstOrNull { it.notifKey == isolatedNotif }
-                    }
+            .distinctUntilChanged()
+            .sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion ->
                 val animate =
                     when {
-                        isolatedNotif == prev -> false
-                        isolatedNotif == null || prev == null -> shadeExpansion == 0f
+                        iconInfo?.notifKey == prev?.notifKey -> false
+                        iconInfo == null || prev == null -> shadeExpansion == 0f
                         else -> false
                     }
                 AnimatableEvent(iconInfo, animate)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 7667e17..5cdead4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
@@ -40,6 +41,7 @@
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
         falsingManager: FalsingManager,
+        iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
         notificationIconAreaController: NotificationIconAreaController,
         shelfIconViewStore: ShelfNotificationIconViewStore,
     ) {
@@ -51,6 +53,7 @@
                     viewModel.icons,
                     configuration,
                     configurationController,
+                    iconViewBindingFailureTracker,
                     shelfIconViewStore,
                 )
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a0ffba3..14ec08f35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -253,6 +253,7 @@
     private NotificationLogger.OnChildLocationsChangedListener mListener;
     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
     private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
+    private Runnable mOnHeightChangedRunnable;
     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
@@ -1121,6 +1122,10 @@
         if (mOnHeightChangedListener != null) {
             mOnHeightChangedListener.onHeightChanged(view, needsAnimation);
         }
+
+        if (mOnHeightChangedRunnable != null) {
+            mOnHeightChangedRunnable.run();
+        }
     }
 
     public boolean isPulseExpanding() {
@@ -4252,6 +4257,10 @@
         this.mOnHeightChangedListener = onHeightChangedListener;
     }
 
+    void setOnHeightChangedRunnable(Runnable r) {
+        this.mOnHeightChangedRunnable = r;
+    }
+
     void onChildAnimationFinished() {
         setAnimationRunning(false);
         requestChildrenUpdate();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 99b3a00..2cf0c26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -20,6 +20,7 @@
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
 import static com.android.app.animation.Interpolators.STANDARD;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -34,7 +35,6 @@
 
 import android.animation.ObjectAnimator;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.Point;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -65,9 +65,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
@@ -94,7 +92,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -112,7 +109,6 @@
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -122,11 +118,9 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -960,6 +954,13 @@
         mView.setOnHeightChangedListener(listener);
     }
 
+    /**
+     * Invoked in addition to {@see #setOnHeightChangedListener}
+     */
+    public void setOnHeightChangedRunnable(Runnable r) {
+        mView.setOnHeightChangedRunnable(r);
+    }
+
     public void setOverscrollTopChangedListener(
             OnOverscrollTopChangedListener listener) {
         mView.setOverscrollTopChangedListener(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 57cea5d..eb1c17a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -18,13 +18,15 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.Context
-import com.android.systemui.res.R
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -43,6 +45,10 @@
     private val _topPosition = MutableStateFlow(0f)
     val topPosition = _topPosition.asStateFlow()
 
+    private val _notificationStackChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+    /** An internal modification was made to notifications */
+    val notificationStackChanged = _notificationStackChanged.asSharedFlow()
+
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         configurationRepository.onAnyConfigurationChange
             .onStart { emit(Unit) }
@@ -72,6 +78,11 @@
         _topPosition.value = top
     }
 
+    /** An internal modification was made to notifications */
+    fun notificationStackChanged() {
+        _notificationStackChanged.tryEmit(Unit)
+    }
+
     data class ConfigurationBasedDimensions(
         val useSplitShade: Boolean,
         val useLargeScreenHeader: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6cf5610..a5b87f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -45,6 +46,7 @@
     private val configurationController: ConfigurationController,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
+    private val iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val shelfIconViewStore: ShelfNotificationIconViewStore,
 ) {
 
@@ -68,6 +70,7 @@
             configuration,
             configurationController,
             falsingManager,
+            iconViewBindingFailureTracker,
             iconAreaController,
             shelfIconViewStore,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index a1a0cca..0ff1bec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import kotlinx.coroutines.DisposableHandle
@@ -33,6 +34,7 @@
         view: SharedNotificationContainer,
         viewModel: SharedNotificationContainerViewModel,
         controller: NotificationStackScrollLayoutController,
+        notificationStackSizeCalculator: NotificationStackSizeCalculator,
     ): DisposableHandle {
         val disposableHandle =
             view.repeatWhenAttached {
@@ -54,9 +56,16 @@
                     }
 
                     launch {
-                        viewModel.maxNotifications.collect {
-                            controller.setMaxDisplayedNotifications(it)
-                        }
+                        viewModel
+                            .getMaxNotifications { space ->
+                                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
+                                    controller.getView(),
+                                    space,
+                                    0f, // Vertical space for shelf is already accounted for
+                                    controller.getShelfHeight().toFloat(),
+                                )
+                            }
+                            .collect { controller.setMaxDisplayedNotifications(it) }
                     }
 
                     launch {
@@ -70,9 +79,12 @@
                 }
             }
 
+        controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+
         return object : DisposableHandle {
             override fun dispose() {
                 disposableHandle.dispose()
+                controller.setOnHeightChangedRunnable(null)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index b86b5dc..d6b6f75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -22,28 +22,26 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 
 /** View-model for the shared notification container, used by both the shade and keyguard spaces */
 class SharedNotificationContainerViewModel
 @Inject
 constructor(
-    interactor: SharedNotificationContainerInteractor,
+    private val interactor: SharedNotificationContainerInteractor,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    controller: NotificationStackScrollLayoutController,
-    shadeInteractor: ShadeInteractor,
+    private val shadeInteractor: ShadeInteractor,
 ) {
     private val statesForConstrainedNotifications =
         setOf(
@@ -151,24 +149,46 @@
      * When on keyguard, there is limited space to display notifications so calculate how many could
      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
      *
-     * TODO: b/296606746 - Need to rerun logic when notifs change
+     * When expanding or when the user is interacting with the shade, keep the count stable; do not
+     * emit a value.
      */
-    val maxNotifications: Flow<Int> =
-        combine(isOnLockscreen, shadeInteractor.shadeExpansion, position) {
-            onLockscreen,
-            shadeExpansion,
-            positionInfo ->
-            if (onLockscreen && shadeExpansion < 1f) {
-                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                    controller.getView(),
-                    positionInfo.bottom - positionInfo.top,
-                    0f, // Vertical space for shelf is already accounted for
-                    controller.getShelfHeight().toFloat(),
-                )
-            } else {
-                -1 // No limit
+    fun getMaxNotifications(calculateSpace: (Float) -> Int): Flow<Int> {
+        // When to limit notifications: on lockscreen with an unexpanded shade. Also, recalculate
+        // when the notification stack has changed internally
+        val limitedNotifications =
+            combineTransform(
+                isOnLockscreen,
+                position,
+                shadeInteractor.shadeExpansion,
+                interactor.notificationStackChanged.onStart { emit(Unit) },
+            ) { isOnLockscreen, position, shadeExpansion, _ ->
+                if (isOnLockscreen && shadeExpansion == 0f) {
+                    emit(calculateSpace(position.bottom - position.top))
+                }
             }
-        }
+
+        // When to show unlimited notifications: When the shade is fully expanded and the user is
+        // not actively dragging the shade
+        val unlimitedNotifications =
+            combineTransform(
+                shadeInteractor.shadeExpansion,
+                shadeInteractor.isUserInteracting,
+            ) { shadeExpansion, isUserInteracting ->
+                if (shadeExpansion == 1f && !isUserInteracting) {
+                    emit(-1)
+                }
+            }
+
+        return merge(
+                limitedNotifications,
+                unlimitedNotifications,
+            )
+            .distinctUntilChanged()
+    }
+
+    fun notificationStackChanged() {
+        interactor.notificationStackChanged()
+    }
 
     data class ConfigurationBasedDimensions(
         val marginStart: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/StatusBarNotificationViewBinderModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/StatusBarNotificationViewBinderModule.kt
new file mode 100644
index 0000000..bcf7322
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/StatusBarNotificationViewBinderModule.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.statusbar.notification.ui.viewbinder
+
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import dagger.Module
+
+@Module(includes = [StatusBarIconViewBindingFailureTracker.Module::class])
+object StatusBarNotificationViewBinderModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index beeee1b..495b4e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -252,7 +252,7 @@
             }
             if (NotificationIconContainerRefactor.isEnabled()) {
                 mHeadsUpNotificationIconInteractor.setIsolatedIconNotificationKey(
-                        newEntry == null ? null : newEntry.getKey());
+                        newEntry == null ? null : newEntry.getRepresentativeEntry().getKey());
             } else {
                 updateIsolatedIconLocation(false /* requireUpdate */);
                 mNotificationIconAreaController.showIconIsolated(newEntry == null ? null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 3921e69..7adc08c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -56,6 +56,7 @@
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
@@ -217,6 +218,7 @@
         mWaitingForWindowStateChangeAfterCameraLaunch = false;
         mTransitionFromLockscreenToDreamStarted = false;
     };
+    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
 
     @Inject
     public CollapsedStatusBarFragment(
@@ -235,6 +237,7 @@
             KeyguardStateController keyguardStateController,
             ShadeViewController shadeViewController,
             StatusBarStateController statusBarStateController,
+            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
             CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
@@ -264,6 +267,7 @@
         mKeyguardStateController = keyguardStateController;
         mShadeViewController = shadeViewController;
         mStatusBarStateController = statusBarStateController;
+        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
         mCommandQueue = commandQueue;
         mCarrierConfigTracker = carrierConfigTracker;
         mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
@@ -471,6 +475,7 @@
                     mStatusBarIconsViewModel,
                     mConfigurationState,
                     mConfigurationController,
+                    mIconViewBindingFailureTracker,
                     mStatusBarIconViewStore);
         } else {
             mNotificationIconAreaInner =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/StatusBarViewBinderModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/StatusBarViewBinderModule.kt
new file mode 100644
index 0000000..4cbdd6f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/StatusBarViewBinderModule.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.statusbar.ui.binder
+
+import com.android.systemui.statusbar.notification.ui.viewbinder.StatusBarNotificationViewBinderModule
+import dagger.Module
+
+@Module(includes = [StatusBarNotificationViewBinderModule::class]) object StatusBarViewBinderModule
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
index db4ab7e..5b91615 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -25,6 +25,16 @@
  * The fraction after which we start fading in when going from a gone widget to a visible one
  */
 private const val GONE_FADE_FRACTION = 0.8f
+/**
+ * The fraction after which we start fading in going from a gone widget to a visible one in guts
+ * animation.
+ */
+private const val GONE_FADE_GUTS_FRACTION = 0.286f
+/**
+ * The fraction before which we fade out when going from a visible widget to a gone one in guts
+ * animation.
+ */
+private const val VISIBLE_FADE_GUTS_FRACTION = 0.355f
 
 /**
  * The amont we're scaling appearing views
@@ -45,6 +55,7 @@
     private var animationStartState: TransitionViewState? = null
     private var state = TransitionViewState()
     private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+    private var isGutsAnimation = false
     private var currentHeight: Int = 0
     private var currentWidth: Int = 0
     var sizeChangedListener: ((Int, Int) -> Unit)? = null
@@ -152,15 +163,6 @@
                 // this looks quite ugly
                 val nowGone: Boolean
                 if (widgetStart.gone) {
-
-                    // Only fade it in at the very end
-                    alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
-                    nowGone = progress < GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val endScale = widgetEnd.scale
-                    newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
-
                     // don't clip
                     widthProgress = 1.0f
 
@@ -168,25 +170,52 @@
                     resultMeasureWidth = widgetEnd.measureWidth
                     resultMeasureHeight = widgetEnd.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
-                            widgetEnd.x,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
-                            widgetEnd.y,
-                            progress)
+                    if (isGutsAnimation) {
+                        // if animation is open/close guts, fade in starts early.
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_GUTS_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetStart.x
+                        resultY = widgetStart.y
+                    } else {
+                        // Only fade it in at the very end
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val endScale = widgetEnd.scale
+                        newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x - resultMeasureWidth / 2.0f,
+                                widgetEnd.x,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y - resultMeasureHeight / 2.0f,
+                                widgetEnd.y,
+                                progress
+                        )
+                    }
                 } else {
-
-                    // Fadeout in the very beginning
-                    alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
-                            progress)
-                    nowGone = progress > 1.0f - GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val startScale = widgetStart.scale
-                    newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
-
                     // Don't clip
                     widthProgress = 0.0f
 
@@ -194,14 +223,54 @@
                     resultMeasureWidth = widgetStart.measureWidth
                     resultMeasureHeight = widgetStart.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x,
-                            widgetEnd.x - resultMeasureWidth / 2.0f,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y,
-                            widgetEnd.y - resultMeasureHeight / 2.0f,
-                            progress)
+                    // Fadeout in the very beginning
+                    if (isGutsAnimation) {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            VISIBLE_FADE_GUTS_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > VISIBLE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget during open/close guts animation.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetEnd.x
+                        resultY = widgetEnd.y
+                    } else {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            1.0f - GONE_FADE_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > 1.0f - GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val startScale = widgetStart.scale
+                        newScale = MathUtils.lerp(
+                                startScale,
+                                startScale * GONE_SCALE_AMOUNT,
+                                progress
+                        )
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x,
+                                widgetEnd.x - resultMeasureWidth / 2.0f,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y,
+                                widgetEnd.y - resultMeasureHeight / 2.0f,
+                                progress
+                        )
+                    }
                 }
                 resultWidgetState.gone = nowGone
             } else {
@@ -279,8 +348,10 @@
         applyImmediately: Boolean,
         animate: Boolean,
         duration: Long = 0,
-        delay: Long = 0
+        delay: Long = 0,
+        isGuts: Boolean,
     ) {
+        isGutsAnimation = isGuts
         val animated = animate && currentState.width != 0
         this.state = state.copy()
         if (applyImmediately || transitionLayout == null) {
@@ -291,6 +362,8 @@
             animationStartState = currentState.copy()
             animator.duration = duration
             animator.startDelay = delay
+            animator.interpolator =
+                if (isGutsAnimation) Interpolators.LINEAR else Interpolators.FAST_OUT_SLOW_IN
             animator.start()
         } else if (!animator.isRunning) {
             applyStateToLayout(this.state)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 84d2b37..404621d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,7 +34,6 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
@@ -83,7 +82,6 @@
 import android.util.SparseBooleanArray;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
@@ -120,7 +118,6 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.Prefs;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -304,7 +301,6 @@
     private final DevicePostureController mDevicePostureController;
     private @DevicePostureController.DevicePostureInt int mDevicePosture;
     private int mOrientation;
-    private final FeatureFlags mFeatureFlags;
     private final Lazy<SecureSettings> mSecureSettings;
     private int mDialogTimeoutMillis;
 
@@ -323,9 +319,7 @@
             DevicePostureController devicePostureController,
             Looper looper,
             DumpManager dumpManager,
-            FeatureFlags featureFlags,
             Lazy<SecureSettings> secureSettings) {
-        mFeatureFlags = featureFlags;
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mHandler = new H(looper);
@@ -1373,14 +1367,12 @@
 
     private void provideTouchFeedbackH(int newRingerMode) {
         VibrationEffect effect = null;
-        int hapticConstant = HapticFeedbackConstants.NO_HAPTICS;
         switch (newRingerMode) {
             case RINGER_MODE_NORMAL:
                 mController.scheduleTouchFeedback();
                 break;
             case RINGER_MODE_SILENT:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-                hapticConstant = HapticFeedbackConstants.TOGGLE_OFF;
                 break;
             case RINGER_MODE_VIBRATE:
                 // Feedback handled by onStateChange, for feedback both when user toggles
@@ -1388,11 +1380,8 @@
                 break;
             default:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
-                hapticConstant = HapticFeedbackConstants.TOGGLE_ON;
         }
-        if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-            mDialogView.performHapticFeedback(hapticConstant);
-        } else if (effect != null) {
+        if (effect != null) {
             mController.vibrate(effect);
         }
     }
@@ -1820,22 +1809,7 @@
                 && mState.ringerModeInternal != -1
                 && mState.ringerModeInternal != state.ringerModeInternal
                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
-
-            if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-                if (mShowing) {
-                    // The dialog view is responsible for triggering haptics in the oneway API
-                    mDialogView.performHapticFeedback(HapticFeedbackConstants.TOGGLE_ON);
-                }
-                /*
-                TODO(b/290642122): If the dialog is not showing, we have the case where haptics is
-                enabled by dragging the volume slider of Settings to a value of 0. This must be
-                handled by view Slices in Settings whilst using the performHapticFeedback API.
-                 */
-
-            } else {
-                // Old behavior only active if the oneway API is not used.
-                mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
-            }
+            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
         }
         mState = state;
         mDynamic.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index e3b3c21..53217d4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -22,7 +22,6 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -65,7 +64,6 @@
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
             DumpManager dumpManager,
-            FeatureFlags featureFlags,
             Lazy<SecureSettings> secureSettings) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
@@ -82,7 +80,6 @@
                 devicePostureController,
                 Looper.getMainLooper(),
                 dumpManager,
-                featureFlags,
                 secureSettings);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index a38ba00..1d8a664 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -60,6 +60,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -192,6 +193,7 @@
                 mSmartspaceController,
                 mock(ConfigurationController.class),
                 mock(ScreenOffAnimationController.class),
+                mock(StatusBarIconViewBindingFailureTracker.class),
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
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
index ae2ec2c..87ab5b0 100644
--- 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
@@ -143,6 +143,23 @@
             assertThat(authenticationChallengeResults).isEqualTo(listOf(true, false, true))
         }
 
+    @Test
+    fun isPinEnhancedPrivacyEnabled() =
+        testScope.runTest {
+            whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[0].id))
+                .thenReturn(false)
+            whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[1].id))
+                .thenReturn(true)
+
+            val values by collectValues(underTest.isPinEnhancedPrivacyEnabled)
+            assertThat(values.first()).isTrue()
+            assertThat(values.last()).isFalse()
+
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+            assertThat(values.last()).isTrue()
+
+    }
+
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 6da6951..7a9cb6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -354,6 +354,18 @@
             assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
         }
 
+    @Test
+    fun isDigitButtonAnimationEnabled() =
+        testScope.runTest {
+            val isAnimationEnabled by collectLastValue(underTest.isDigitButtonAnimationEnabled)
+
+            utils.authenticationRepository.setPinEnhancedPrivacyEnabled(true)
+            assertThat(isAnimationEnabled).isFalse()
+
+            utils.authenticationRepository.setPinEnhancedPrivacyEnabled(false)
+            assertThat(isAnimationEnabled).isTrue()
+        }
+
     private fun TestScope.lockDeviceAndOpenPinBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
         utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index a6c4f19..af4bf36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -240,7 +240,7 @@
         }
 
     @Test
-    fun contentOrdering() =
+    fun ordering_smartspaceBeforeUmoBeforeWidgets() =
         testScope.runTest {
             tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractorTest.kt
new file mode 100644
index 0000000..e8eda80
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryUdfpsInteractorTest.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.deviceentry.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceEntryUdfpsInteractorTest : SysuiTestCase() {
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository
+    private lateinit var biometricsRepository: FakeBiometricSettingsRepository
+
+    private lateinit var underTest: DeviceEntryUdfpsInteractor
+
+    @Before
+    fun setUp() {
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+        biometricsRepository = FakeBiometricSettingsRepository()
+
+        underTest =
+            DeviceEntryUdfpsInteractor(
+                fingerprintPropertyRepository = fingerprintPropertyRepository,
+                fingerprintAuthRepository = fingerprintAuthRepository,
+                biometricSettingsRepository = biometricsRepository,
+            )
+    }
+
+    @Test
+    fun udfpsSupported_rearFp_false() = runTest {
+        val isUdfpsSupported by collectLastValue(underTest.isUdfpsSupported)
+        fingerprintPropertyRepository.supportsRearFps()
+        assertThat(isUdfpsSupported).isFalse()
+    }
+
+    @Test
+    fun udfpsSupoprted() = runTest {
+        val isUdfpsSupported by collectLastValue(underTest.isUdfpsSupported)
+        fingerprintPropertyRepository.supportsUdfps()
+        assertThat(isUdfpsSupported).isTrue()
+    }
+
+    @Test
+    fun udfpsEnrolledAndEnabled() = runTest {
+        val isUdfpsEnrolledAndEnabled by collectLastValue(underTest.isUdfpsEnrolledAndEnabled)
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        assertThat(isUdfpsEnrolledAndEnabled).isTrue()
+    }
+
+    @Test
+    fun udfpsEnrolledAndEnabled_rearFp_false() = runTest {
+        val isUdfpsEnrolledAndEnabled by collectLastValue(underTest.isUdfpsEnrolledAndEnabled)
+        fingerprintPropertyRepository.supportsRearFps()
+        biometricsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        assertThat(isUdfpsEnrolledAndEnabled).isFalse()
+    }
+
+    @Test
+    fun udfpsEnrolledAndEnabled_notEnrolledOrEnabled_false() = runTest {
+        val isUdfpsEnrolledAndEnabled by collectLastValue(underTest.isUdfpsEnrolledAndEnabled)
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        assertThat(isUdfpsEnrolledAndEnabled).isFalse()
+    }
+
+    @Test
+    fun isListeningForUdfps() = runTest {
+        val isListeningForUdfps by collectLastValue(underTest.isListeningForUdfps)
+        fingerprintPropertyRepository.supportsUdfps()
+        fingerprintAuthRepository.setIsRunning(true)
+        assertThat(isListeningForUdfps).isTrue()
+    }
+
+    @Test
+    fun isListeningForUdfps_rearFp_false() = runTest {
+        val isListeningForUdfps by collectLastValue(underTest.isListeningForUdfps)
+        fingerprintPropertyRepository.supportsRearFps()
+        fingerprintAuthRepository.setIsRunning(true)
+        assertThat(isListeningForUdfps).isFalse()
+    }
+
+    @Test
+    fun isListeningForUdfps_notRunning_false() = runTest {
+        val isListeningForUdfps by collectLastValue(underTest.isListeningForUdfps)
+        fingerprintPropertyRepository.supportsUdfps()
+        fingerprintAuthRepository.setIsRunning(false)
+        assertThat(isListeningForUdfps).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
index 5eab2fc..df52265 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
@@ -84,8 +84,8 @@
     @Test
     fun udfpsBurnInOffset_updatesOnResolutionScaleChange() =
         testScope.runTest {
-            val udfpsBurnInOffsetX by collectLastValue(underTest.udfpsBurnInXOffset)
-            val udfpsBurnInOffsetY by collectLastValue(underTest.udfpsBurnInYOffset)
+            val udfpsBurnInOffsetX by collectLastValue(underTest.deviceEntryIconXOffset)
+            val udfpsBurnInOffsetY by collectLastValue(underTest.deviceEntryIconYOffset)
             assertThat(udfpsBurnInOffsetX).isEqualTo(burnInOffset)
             assertThat(udfpsBurnInOffsetY).isEqualTo(burnInOffset)
 
@@ -101,7 +101,7 @@
     @Test
     fun udfpsBurnInProgress_updatesOnDozeTimeTick() =
         testScope.runTest {
-            val udfpsBurnInProgress by collectLastValue(underTest.udfpsBurnInProgress)
+            val udfpsBurnInProgress by collectLastValue(underTest.udfpsProgress)
             assertThat(udfpsBurnInProgress).isEqualTo(burnInProgress)
 
             setBurnInProgress(.88f)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
index 3442df6..2dfc132 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -123,30 +123,30 @@
             runCurrent()
 
             // THEN burn in offsets are 0
-            assertThat(burnInOffsets?.burnInProgress).isEqualTo(0f)
-            assertThat(burnInOffsets?.burnInYOffset).isEqualTo(0)
-            assertThat(burnInOffsets?.burnInXOffset).isEqualTo(0)
+            assertThat(burnInOffsets?.progress).isEqualTo(0f)
+            assertThat(burnInOffsets?.y).isEqualTo(0)
+            assertThat(burnInOffsets?.x).isEqualTo(0)
 
             // WHEN we're in the middle of the doze amount change
             keyguardRepository.setDozeAmount(.50f)
             runCurrent()
 
             // THEN burn in is updated (between 0 and the full offset)
-            assertThat(burnInOffsets?.burnInProgress).isGreaterThan(0f)
-            assertThat(burnInOffsets?.burnInYOffset).isGreaterThan(0)
-            assertThat(burnInOffsets?.burnInXOffset).isGreaterThan(0)
-            assertThat(burnInOffsets?.burnInProgress).isLessThan(burnInProgress)
-            assertThat(burnInOffsets?.burnInYOffset).isLessThan(burnInYOffset)
-            assertThat(burnInOffsets?.burnInXOffset).isLessThan(burnInXOffset)
+            assertThat(burnInOffsets?.progress).isGreaterThan(0f)
+            assertThat(burnInOffsets?.y).isGreaterThan(0)
+            assertThat(burnInOffsets?.x).isGreaterThan(0)
+            assertThat(burnInOffsets?.progress).isLessThan(burnInProgress)
+            assertThat(burnInOffsets?.y).isLessThan(burnInYOffset)
+            assertThat(burnInOffsets?.x).isLessThan(burnInXOffset)
 
             // WHEN we're fully dozing
             keyguardRepository.setDozeAmount(1f)
             runCurrent()
 
             // THEN burn in offsets are updated to final current values (for the given time)
-            assertThat(burnInOffsets?.burnInProgress).isEqualTo(burnInProgress)
-            assertThat(burnInOffsets?.burnInYOffset).isEqualTo(burnInYOffset)
-            assertThat(burnInOffsets?.burnInXOffset).isEqualTo(burnInXOffset)
+            assertThat(burnInOffsets?.progress).isEqualTo(burnInProgress)
+            assertThat(burnInOffsets?.y).isEqualTo(burnInYOffset)
+            assertThat(burnInOffsets?.x).isEqualTo(burnInXOffset)
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
index 71313c8..75bdcdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
@@ -24,12 +24,14 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
-import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
@@ -42,6 +44,7 @@
 import org.junit.runners.JUnit4
 import org.mockito.Answers
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
 @ExperimentalCoroutinesApi
@@ -77,7 +80,9 @@
                 notificationPanelView,
                 featureFlags,
                 { lockIconViewController },
-                { DeviceEntryIconViewModel() },
+                { mock(DeviceEntryIconViewModel::class.java) },
+                { mock(DeviceEntryForegroundViewModel::class.java) },
+                { mock(DeviceEntryBackgroundViewModel::class.java) },
                 { falsingManager },
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelTest.kt
new file mode 100644
index 0000000..f282481
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodToGoneTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: AodToGoneTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
+        underTest = AodToGoneTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun deviceEntryParentViewHides() = runTest {
+        val deviceEntryParentViewAlpha by collectValues(underTest.deviceEntryParentViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        repository.sendTransitionStep(step(0.1f))
+        repository.sendTransitionStep(step(0.3f))
+        repository.sendTransitionStep(step(0.4f))
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(0.6f))
+        repository.sendTransitionStep(step(0.8f))
+        repository.sendTransitionStep(step(1f))
+        deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) }
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.AOD,
+            to = KeyguardState.GONE,
+            value = value,
+            transitionState = state,
+            ownerName = "AodToGoneTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..517149c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: AodToLockscreenTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        underTest =
+            AodToLockscreenTransitionViewModel(
+                interactor =
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = TestScope().backgroundScope,
+                            repository = repository,
+                        )
+                        .keyguardTransitionInteractor,
+                deviceEntryUdfpsInteractor =
+                    DeviceEntryUdfpsInteractor(
+                        fingerprintPropertyRepository = fingerprintPropertyRepository,
+                        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                        biometricSettingsRepository = FakeBiometricSettingsRepository(),
+                    ),
+            )
+    }
+
+    @Test
+    fun deviceEntryParentViewShows() = runTest {
+        val deviceEntryParentViewAlpha by collectValues(underTest.deviceEntryParentViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        repository.sendTransitionStep(step(0.1f))
+        repository.sendTransitionStep(step(0.3f))
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(0.6f))
+        repository.sendTransitionStep(step(1f))
+        deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(1f) }
+    }
+
+    @Test
+    fun deviceEntryBackgroundView_udfps_alphaFadeIn() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        val deviceEntryBackgroundViewAlpha by
+            collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+
+        // fade in
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(0.1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(.2f)
+
+        repository.sendTransitionStep(step(0.3f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(.6f)
+
+        repository.sendTransitionStep(step(0.6f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun deviceEntryBackgroundView_rearFp_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsRearFps()
+        val deviceEntryBackgroundViewAlpha by
+            collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+        // no updates
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryBackgroundViewAlpha).isNull()
+        repository.sendTransitionStep(step(0.1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isNull()
+        repository.sendTransitionStep(step(0.3f))
+        assertThat(deviceEntryBackgroundViewAlpha).isNull()
+        repository.sendTransitionStep(step(0.6f))
+        assertThat(deviceEntryBackgroundViewAlpha).isNull()
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isNull()
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.AOD,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "AodToLockscreenTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelTest.kt
new file mode 100644
index 0000000..96f69462
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+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.google.common.truth.Truth
+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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodToOccludedTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: AodToOccludedTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
+        underTest = AodToOccludedTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun deviceEntryParentViewHides() = runTest {
+        val deviceEntryParentViewAlpha by collectValues(underTest.deviceEntryParentViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        repository.sendTransitionStep(step(0.1f))
+        repository.sendTransitionStep(step(0.3f))
+        repository.sendTransitionStep(step(0.4f))
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(0.6f))
+        repository.sendTransitionStep(step(0.8f))
+        repository.sendTransitionStep(step(1f))
+        deviceEntryParentViewAlpha.forEach { Truth.assertThat(it).isEqualTo(0f) }
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.AOD,
+            to = KeyguardState.OCCLUDED,
+            value = value,
+            transitionState = state,
+            ownerName = "AodToOccludedTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..5dccc3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DozingToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var testScope: TestScope
+    private lateinit var underTest: DozingToLockscreenTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        underTest =
+            DozingToLockscreenTransitionViewModel(
+                interactor =
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = TestScope().backgroundScope,
+                            repository = repository,
+                        )
+                        .keyguardTransitionInteractor,
+            )
+    }
+
+    @Test
+    fun deviceEntryParentViewShows() = runTest {
+        val deviceEntryParentViewAlpha by collectValues(underTest.deviceEntryParentViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        repository.sendTransitionStep(step(0.1f))
+        repository.sendTransitionStep(step(0.3f))
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(0.6f))
+        repository.sendTransitionStep(step(1f))
+        deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(1f) }
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.DOZING,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "DozingToLockscreenTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 6d47aed..fd125e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -19,6 +19,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+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.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -35,6 +41,7 @@
 import com.android.systemui.util.mockito.mock
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -44,22 +51,34 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: DreamingToLockscreenTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
 
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
         val interactor =
             KeyguardTransitionInteractorFactory.create(
                     scope = TestScope().backgroundScope,
                     repository = repository,
                 )
                 .keyguardTransitionInteractor
-        underTest = DreamingToLockscreenTransitionViewModel(interactor, mock())
+        underTest =
+            DreamingToLockscreenTransitionViewModel(
+                interactor,
+                mock(),
+                DeviceEntryUdfpsInteractor(
+                    fingerprintPropertyRepository = fingerprintPropertyRepository,
+                    fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                    biometricSettingsRepository = FakeBiometricSettingsRepository(),
+                ),
+            )
     }
 
     @Test
@@ -129,6 +148,78 @@
         }
 
     @Test
+    fun deviceEntryParentViewFadeIn() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.deviceEntryParentViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun deviceEntryBackgroundViewAppear() =
+        runTest(UnconfinedTestDispatcher()) {
+            fingerprintPropertyRepository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations = emptyMap(),
+            )
+            val values = mutableListOf<Float>()
+
+            val job =
+                underTest.deviceEntryBackgroundViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(1f))
+
+            values.forEach { assertThat(it).isEqualTo(1f) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun deviceEntryBackground_noUdfps_noUpdates() =
+        runTest(UnconfinedTestDispatcher()) {
+            fingerprintPropertyRepository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.REAR,
+                sensorLocations = emptyMap(),
+            )
+            val values = mutableListOf<Float>()
+
+            val job =
+                underTest.deviceEntryBackgroundViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(0) // no updates
+
+            job.cancel()
+        }
+
+    @Test
     fun lockscreenTranslationY() =
         runTest(UnconfinedTestDispatcher()) {
             val values = mutableListOf<Float>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index 255f4df..c1444a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -19,7 +19,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -27,6 +31,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
@@ -34,11 +39,14 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class GoneToAodTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: GoneToAodTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
     private lateinit var testScope: TestScope
 
     @Before
@@ -47,13 +55,24 @@
         testScope = TestScope(testDispatcher)
 
         repository = FakeKeyguardTransitionRepository()
-        val interactor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = testScope.backgroundScope,
-                    repository = repository,
-                )
-                .keyguardTransitionInteractor
-        underTest = GoneToAodTransitionViewModel(interactor)
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+
+        underTest =
+            GoneToAodTransitionViewModel(
+                interactor =
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = testScope.backgroundScope,
+                            repository = repository,
+                        )
+                        .keyguardTransitionInteractor,
+                deviceEntryUdfpsInteractor =
+                    DeviceEntryUdfpsInteractor(
+                        fingerprintPropertyRepository = fingerprintPropertyRepository,
+                        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                        biometricSettingsRepository = biometricSettingsRepository,
+                    ),
+            )
     }
 
     @Test
@@ -63,11 +82,11 @@
             val enterFromTopTranslationY by
                 collectLastValue(underTest.enterFromTopTranslationY(pixels.toInt()))
 
-            // The animation should only start > halfway through
+            // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(enterFromTopTranslationY).isEqualTo(pixels)
 
-            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.4f))
             assertThat(enterFromTopTranslationY).isEqualTo(pixels)
 
             repository.sendTransitionStep(step(.85f))
@@ -83,11 +102,11 @@
         testScope.runTest {
             val enterFromTopAnimationAlpha by collectLastValue(underTest.enterFromTopAnimationAlpha)
 
-            // The animation should only start > halfway through
+            // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(enterFromTopAnimationAlpha).isEqualTo(0f)
 
-            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.4f))
             assertThat(enterFromTopAnimationAlpha).isEqualTo(0f)
 
             repository.sendTransitionStep(step(.85f))
@@ -97,6 +116,98 @@
             assertThat(enterFromTopAnimationAlpha).isEqualTo(1f)
         }
 
+    @Test
+    fun deviceEntryBackgroundViewAlpha() =
+        testScope.runTest {
+            val deviceEntryBackgroundViewAlpha by
+                collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+
+            // immediately 0f
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.4f))
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.85f))
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsEnrolled() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsUdfps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+            // animation doesn't start until the end
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.5f))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.95f))
+            assertThat(deviceEntryParentViewAlpha).isIn(Range.closed(.01f, 1f))
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(deviceEntryParentViewAlpha).isIn(Range.closed(.99f, 1f))
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_rearFpEnrolled() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsRearFps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+            // animation doesn't start until the end
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(0.5f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(.95f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsNotEnrolled_noUpdates() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsUdfps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+            // animation doesn't start until the end
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(0.5f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(.95f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(deviceEntryParentViewAlpha).isNull()
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
new file mode 100644
index 0000000..4074851
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.SysUITestComponent
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.collectLastValue
+import com.android.collectValues
+import com.android.runCurrent
+import com.android.runTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
+import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToAodTransitionViewModelTest : SysuiTestCase() {
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<LockscreenToAodTransitionViewModel> {
+        val repository: FakeKeyguardTransitionRepository
+        val deviceEntryRepository: FakeDeviceEntryRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val shadeRepository: FakeShadeRepository
+        val fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+        val biometricSettingsRepository: FakeBiometricSettingsRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+
+        fun shadeExpanded(expanded: Boolean) {
+            if (expanded) {
+                shadeRepository.setQsExpansion(1f)
+            } else {
+                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+                shadeRepository.setQsExpansion(0f)
+                shadeRepository.setLockscreenShadeExpansion(0f)
+            }
+        }
+    }
+
+    private val testComponent: TestComponent =
+        DaggerLockscreenToAodTransitionViewModelTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(FACE_AUTH_REFACTOR, true)
+                        set(FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks = TestMocksModule(),
+            )
+
+    @Test
+    fun backgroundViewAlpha_shadeNotExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
+            // fade out
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(.3f))
+            assertThat(actual).isIn(Range.closed(.1f, .9f))
+
+            // finish fading out before the end of the full transition
+            repository.sendTransitionStep(step(.7f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(0f)
+        }
+
+    @Test
+    fun backgroundViewAlpha_shadeExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            // immediately 0f
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.3f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.7f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(0f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsEnrolled_shadeNotExpanded() =
+        testComponent.runTest {
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            fingerprintPropertyRepository.supportsUdfps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            shadeExpanded(false)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED),
+                        step(.3f),
+                        step(.7f),
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
+            // immediately 1f
+            values.forEach { assertThat(it).isEqualTo(1f) }
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsEnrolled_shadeExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            fingerprintPropertyRepository.supportsUdfps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            shadeExpanded(true)
+            runCurrent()
+
+            // fade in
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.3f))
+            assertThat(actual).isIn(Range.closed(.1f, .9f))
+
+            // finish fading in before the end of the full transition
+            repository.sendTransitionStep(step(.7f))
+            assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(1f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_rearFp_shadeNotExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            fingerprintPropertyRepository.supportsRearFps()
+            shadeExpanded(false)
+            runCurrent()
+
+            // fade out
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(.1f))
+            assertThat(actual).isIn(Range.closed(.1f, .9f))
+
+            // finish fading out before the end of the full transition
+            repository.sendTransitionStep(step(.7f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(0f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_rearFp_shadeExpanded() =
+        testComponent.runTest {
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            fingerprintPropertyRepository.supportsRearFps()
+            shadeExpanded(true)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED),
+                        step(.3f),
+                        step(.7f),
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
+            // immediately 0f
+            values.forEach { assertThat(it).isEqualTo(0f) }
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.AOD,
+            value = value,
+            transitionState = state,
+            ownerName = "LockscreenToAodTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 89a1d2b..5c85357 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,88 +18,172 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.SysUITestComponent
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.collectLastValue
+import com.android.collectValues
+import com.android.runCurrent
+import com.android.runTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 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.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import dagger.BindsInstance
+import dagger.Component
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
-    private lateinit var underTest: LockscreenToDreamingTransitionViewModel
-    private lateinit var repository: FakeKeyguardTransitionRepository
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<LockscreenToDreamingTransitionViewModel> {
+        val repository: FakeKeyguardTransitionRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val shadeRepository: FakeShadeRepository
 
-    @Before
-    fun setUp() {
-        repository = FakeKeyguardTransitionRepository()
-        val interactor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = TestScope().backgroundScope,
-                    repository = repository,
-                )
-                .keyguardTransitionInteractor
-        underTest = LockscreenToDreamingTransitionViewModel(interactor)
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+
+        fun shadeExpanded(expanded: Boolean) {
+            if (expanded) {
+                shadeRepository.setQsExpansion(1f)
+            } else {
+                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+                shadeRepository.setQsExpansion(0f)
+                shadeRepository.setLockscreenShadeExpansion(0f)
+            }
+        }
     }
 
+    private val testComponent: TestComponent =
+        DaggerLockscreenToDreamingTransitionViewModelTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(Flags.FACE_AUTH_REFACTOR, true)
+                        set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks = TestMocksModule(),
+            )
     @Test
     fun lockscreenFadeOut() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-
-            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
-
-            // Should start running here...
-            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            repository.sendTransitionStep(step(0f))
-            repository.sendTransitionStep(step(0.1f))
-            repository.sendTransitionStep(step(0.2f))
-            repository.sendTransitionStep(step(0.3f))
-            // ...up to here
-            repository.sendTransitionStep(step(1f))
+        testComponent.runTest {
+            val values by collectValues(underTest.lockscreenAlpha)
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.1f),
+                        step(.2f),
+                        step(.3f), // ...up to here
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
 
             // Only three values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
             assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
-
-            job.cancel()
         }
 
     @Test
     fun lockscreenTranslationY() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-
+        testComponent.runTest {
             val pixels = 100
-            val job =
-                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+            val values by collectValues(underTest.lockscreenTranslationY(pixels))
 
-            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            repository.sendTransitionStep(step(0f))
-            repository.sendTransitionStep(step(0.3f))
-            repository.sendTransitionStep(step(0.5f))
-            repository.sendTransitionStep(step(1f))
-            // And a final reset event on FINISHED
-            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.3f),
+                        step(.5f),
+                        step(1f),
+                        step(1f, TransitionState.FINISHED), // Final reset event on FINISHED
+                    ),
+                testScope = testScope,
+            )
 
             assertThat(values.size).isEqualTo(6)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
             // Validate finished value
             assertThat(values[5]).isEqualTo(0f)
+        }
 
-            job.cancel()
+    @Test
+    fun deviceEntryParentViewAlpha_shadeExpanded() =
+        testComponent.runTest {
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED),
+                        step(0f),
+                        step(.3f),
+                        step(.5f),
+                        step(1f),
+                        step(1f, TransitionState.FINISHED),
+                    ),
+                testScope = testScope,
+            )
+
+            // immediately 0f
+            values.forEach { assertThat(it).isEqualTo(0f) }
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_shadeNotExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
+            // fade out
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(.1f))
+            assertThat(actual).isIn(Range.open(.1f, .9f))
+
+            // alpha is 1f before the full transition starts ending
+            repository.sendTransitionStep(step(0.8f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(0f)
         }
 
     private fun step(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
new file mode 100644
index 0000000..1494c92
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.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.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToGoneTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: LockscreenToGoneTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
+        underTest =
+            LockscreenToGoneTransitionViewModel(
+                interactor,
+            )
+    }
+
+    @Test
+    fun deviceEntryParentViewHides() = runTest {
+        val deviceEntryParentViewAlpha by collectValues(underTest.deviceEntryParentViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        repository.sendTransitionStep(step(0.1f))
+        repository.sendTransitionStep(step(0.3f))
+        repository.sendTransitionStep(step(0.4f))
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(0.6f))
+        repository.sendTransitionStep(step(0.8f))
+        repository.sendTransitionStep(step(1f))
+        deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) }
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GONE,
+            value = value,
+            transitionState = state,
+            ownerName = "LockscreenToGoneTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 41f8856..4cbefa3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,109 +18,185 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.SysUITestComponent
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.collectLastValue
+import com.android.collectValues
+import com.android.runCurrent
+import com.android.runTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 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.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
-    private lateinit var underTest: LockscreenToOccludedTransitionViewModel
-    private lateinit var repository: FakeKeyguardTransitionRepository
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<LockscreenToOccludedTransitionViewModel> {
+        val repository: FakeKeyguardTransitionRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val shadeRepository: FakeShadeRepository
 
-    @Before
-    fun setUp() {
-        repository = FakeKeyguardTransitionRepository()
-        val interactor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = TestScope().backgroundScope,
-                    repository = repository,
-                )
-                .keyguardTransitionInteractor
-        underTest = LockscreenToOccludedTransitionViewModel(interactor)
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+
+        fun shadeExpanded(expanded: Boolean) {
+            if (expanded) {
+                shadeRepository.setQsExpansion(1f)
+            } else {
+                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+                shadeRepository.setQsExpansion(0f)
+                shadeRepository.setLockscreenShadeExpansion(0f)
+            }
+        }
     }
 
+    private val testComponent: TestComponent =
+        DaggerLockscreenToOccludedTransitionViewModelTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(Flags.FACE_AUTH_REFACTOR, true)
+                        set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks = TestMocksModule(),
+            )
+
     @Test
     fun lockscreenFadeOut() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-
-            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
-
-            // Should start running here...
-            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            repository.sendTransitionStep(step(0f))
-            repository.sendTransitionStep(step(0.1f))
-            repository.sendTransitionStep(step(0.4f))
-            repository.sendTransitionStep(step(0.7f))
-            // ...up to here
-            repository.sendTransitionStep(step(1f))
-
+        testComponent.runTest {
+            val values by collectValues(underTest.lockscreenAlpha)
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.1f),
+                        step(.4f),
+                        step(.7f), // ...up to here
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
             // Only 3 values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
             assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
-
-            job.cancel()
         }
 
     @Test
     fun lockscreenTranslationY() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-
+        testComponent.runTest {
             val pixels = 100
-            val job =
-                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
-
-            // Should start running here...
-            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            repository.sendTransitionStep(step(0f))
-            repository.sendTransitionStep(step(0.3f))
-            repository.sendTransitionStep(step(0.5f))
-            repository.sendTransitionStep(step(1f))
-            // ...up to here
-
+            val values by collectValues(underTest.lockscreenTranslationY(pixels))
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.3f),
+                        step(.5f),
+                        step(1f), // ...up to here
+                    ),
+                testScope = testScope,
+            )
             assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
-
-            job.cancel()
         }
 
     @Test
     fun lockscreenTranslationYIsCanceled() =
-        runTest(UnconfinedTestDispatcher()) {
-            val values = mutableListOf<Float>()
-
+        testComponent.runTest {
             val pixels = 100
-            val job =
-                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
-
-            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            repository.sendTransitionStep(step(0f))
-            repository.sendTransitionStep(step(0.3f))
-            repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
-
+            val values by collectValues(underTest.lockscreenTranslationY(pixels))
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED),
+                        step(0f),
+                        step(.3f),
+                        step(0.3f, TransitionState.CANCELED),
+                    ),
+                testScope = testScope,
+            )
             assertThat(values.size).isEqualTo(4)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
 
             // Cancel will reset the translation
             assertThat(values[3]).isEqualTo(0)
+        }
 
-            job.cancel()
+    @Test
+    fun deviceEntryParentViewAlpha_shadeExpanded() =
+        testComponent.runTest {
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            // immediately 0f
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED),
+                        step(.5f),
+                        step(1f, TransitionState.FINISHED)
+                    ),
+                testScope = testScope,
+            )
+
+            values.forEach { assertThat(it).isEqualTo(0f) }
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_shadeNotExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
+            // fade out
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(.2f))
+            assertThat(actual).isIn(Range.open(.1f, .9f))
+
+            // alpha is 1f before the full transition starts ending
+            repository.sendTransitionStep(step(0.8f))
+            assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(actual).isEqualTo(0f)
         }
 
     private fun step(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
new file mode 100644
index 0000000..4f56435
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.SysUITestComponent
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.collectLastValue
+import com.android.runCurrent
+import com.android.runTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
+import com.google.common.collect.Range
+import com.google.common.truth.Truth
+import dagger.BindsInstance
+import dagger.Component
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() {
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<LockscreenToPrimaryBouncerTransitionViewModel> {
+        val repository: FakeKeyguardTransitionRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val shadeRepository: FakeShadeRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+
+        fun shadeExpanded(expanded: Boolean) {
+            if (expanded) {
+                shadeRepository.setQsExpansion(1f)
+            } else {
+                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+                shadeRepository.setQsExpansion(0f)
+                shadeRepository.setLockscreenShadeExpansion(0f)
+            }
+        }
+    }
+
+    private val testComponent: TestComponent =
+        DaggerLockscreenToPrimaryBouncerTransitionViewModelTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(Flags.FACE_AUTH_REFACTOR, true)
+                        set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks = TestMocksModule(),
+            )
+
+    @Test
+    fun deviceEntryParentViewAlpha_shadeExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            // immediately 0f
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(.2f))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.8f))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha_shadeNotExpanded() =
+        testComponent.runTest {
+            val actual by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
+            // fade out
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(1f)
+
+            repository.sendTransitionStep(step(.1f))
+            runCurrent()
+            Truth.assertThat(actual).isIn(Range.open(.1f, .9f))
+
+            // alpha is 1f before the full transition starts ending
+            repository.sendTransitionStep(step(0.8f))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            runCurrent()
+            Truth.assertThat(actual).isEqualTo(0f)
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.PRIMARY_BOUNCER,
+            value = value,
+            transitionState = state,
+            ownerName = "LockscreenToPrimaryBouncerTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
new file mode 100644
index 0000000..0eb8ff6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OccludedToAodTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: OccludedToAodTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+
+        underTest =
+            OccludedToAodTransitionViewModel(
+                KeyguardTransitionInteractorFactory.create(
+                        scope = TestScope().backgroundScope,
+                        repository = repository,
+                    )
+                    .keyguardTransitionInteractor,
+                DeviceEntryUdfpsInteractor(
+                    fingerprintPropertyRepository = fingerprintPropertyRepository,
+                    fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                    biometricSettingsRepository = biometricSettingsRepository,
+                ),
+            )
+    }
+
+    @Test
+    fun deviceEntryBackgroundViewAlpha() = runTest {
+        val deviceEntryBackgroundViewAlpha by
+            collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+
+        // immediately 0f
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(0.4f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(.85f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsEnrolled() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        // immediately 1f
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(.95f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_rearFpEnrolled_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsRearFps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        // no updates
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(.95f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsNotEnrolled_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        // no updates
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(.95f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.AOD,
+            value = value,
+            transitionState = state,
+            ownerName = "OccludedToAodTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index ec95cb8..d077227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -19,6 +19,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -26,6 +30,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -35,22 +40,35 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: OccludedToLockscreenTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
 
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = TestScope().backgroundScope,
-                    repository = repository,
-                )
-                .keyguardTransitionInteractor
-        underTest = OccludedToLockscreenTransitionViewModel(interactor)
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+        underTest =
+            OccludedToLockscreenTransitionViewModel(
+                interactor =
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = TestScope().backgroundScope,
+                            repository = repository,
+                        )
+                        .keyguardTransitionInteractor,
+                deviceEntryUdfpsInteractor =
+                    DeviceEntryUdfpsInteractor(
+                        fingerprintPropertyRepository = fingerprintPropertyRepository,
+                        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                        biometricSettingsRepository = biometricSettingsRepository,
+                    ),
+            )
     }
 
     @Test
@@ -113,6 +131,78 @@
             job.cancel()
         }
 
+    @Test
+    fun deviceEntryParentViewFadeIn() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.deviceEntryParentViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.1f))
+            // Should start running here...
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.4f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.6f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.8f))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun deviceEntryBackgroundViewShows() =
+        runTest(UnconfinedTestDispatcher()) {
+            fingerprintPropertyRepository.supportsUdfps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            val values = mutableListOf<Float>()
+
+            val job =
+                underTest.deviceEntryBackgroundViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.4f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.6f))
+            repository.sendTransitionStep(step(0.8f))
+            repository.sendTransitionStep(step(1f))
+
+            values.forEach { assertThat(it).isEqualTo(1f) }
+
+            job.cancel()
+        }
+
+    @Test
+    fun deviceEntryBackgroundView_noUdfpsEnrolled_noUpdates() =
+        runTest(UnconfinedTestDispatcher()) {
+            fingerprintPropertyRepository.supportsRearFps()
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            val values = mutableListOf<Float>()
+
+            val job =
+                underTest.deviceEntryBackgroundViewAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.4f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.6f))
+            repository.sendTransitionStep(step(0.8f))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values).isEmpty() // no updates
+
+            job.cancel()
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
new file mode 100644
index 0000000..350b310
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerToAodTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: PrimaryBouncerToAodTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
+        underTest =
+            PrimaryBouncerToAodTransitionViewModel(
+                interactor,
+                DeviceEntryUdfpsInteractor(
+                    fingerprintPropertyRepository = fingerprintPropertyRepository,
+                    fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                    biometricSettingsRepository = biometricSettingsRepository,
+                ),
+            )
+    }
+
+    @Test
+    fun deviceEntryBackgroundViewAlpha() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        val deviceEntryBackgroundViewAlpha by
+            collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+
+        // immediately 0f
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(0.4f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(.85f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryBackgroundViewAlpha).isEqualTo(0f)
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsEnrolled_fadeIn() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+        repository.sendTransitionStep(step(0.5f))
+        repository.sendTransitionStep(step(.75f))
+        repository.sendTransitionStep(step(1f))
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_rearFpEnrolled_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsRearFps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        // animation doesn't start until the end
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(.95f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha_udfpsNotEnrolled_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(.75f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(deviceEntryParentViewAlpha).isNull()
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.AOD,
+            value = value,
+            transitionState = state,
+            ownerName = "PrimaryBouncerToAodTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..24e4920
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+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.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
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: PrimaryBouncerToLockscreenTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+
+        underTest =
+            PrimaryBouncerToLockscreenTransitionViewModel(
+                KeyguardTransitionInteractorFactory.create(
+                        scope = TestScope().backgroundScope,
+                        repository = repository,
+                    )
+                    .keyguardTransitionInteractor,
+                DeviceEntryUdfpsInteractor(
+                    fingerprintPropertyRepository = fingerprintPropertyRepository,
+                    fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository(),
+                    biometricSettingsRepository = biometricSettingsRepository,
+                ),
+            )
+    }
+
+    @Test
+    fun deviceEntryParentViewAlpha() = runTest {
+        val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+        // immediately 1f
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(0.4f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(.85f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun deviceEntryBackgroundViewAlpha_udfpsEnrolled_show() = runTest {
+        fingerprintPropertyRepository.supportsUdfps()
+        val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+
+        // immediately 1f
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(bgViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(0.1f))
+        assertThat(bgViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(.3f))
+        assertThat(bgViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(.5f))
+        assertThat(bgViewAlpha).isEqualTo(1f)
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(bgViewAlpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun deviceEntryBackgroundViewAlpha_rearFpEnrolled_noUpdates() = runTest {
+        fingerprintPropertyRepository.supportsRearFps()
+        val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(bgViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(0.5f))
+        assertThat(bgViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(.75f))
+        assertThat(bgViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f))
+        assertThat(bgViewAlpha).isNull()
+
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(bgViewAlpha).isNull()
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "PrimaryBouncerToLockscreenTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index b101acf..437a35f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -38,8 +38,6 @@
 import com.android.settingslib.media.PhoneMediaDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaDeviceData
@@ -112,7 +110,6 @@
     private lateinit var session: MediaSession
     private lateinit var mediaData: MediaData
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    private val featureFlags = FakeFeatureFlagsClassic()
 
     @Before
     fun setUp() {
@@ -131,7 +128,6 @@
                 fakeFgExecutor,
                 fakeBgExecutor,
                 dumpster,
-                featureFlags,
             )
         manager.addListener(listener)
 
@@ -150,7 +146,6 @@
             MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
         whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
         setupLeAudioConfiguration(false)
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, false)
     }
 
     @After
@@ -463,7 +458,6 @@
 
     @Test
     fun mr2ReturnsSystemRouteWithNullName_isPhone_usePhoneName() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
         // When the routing session name is null, and is a system session for a PhoneMediaDevice
         val phoneDevice = mock(PhoneMediaDevice::class.java)
         whenever(phoneDevice.iconWithoutBackground).thenReturn(icon)
@@ -489,7 +483,6 @@
 
     @Test
     fun mr2ReturnsSystemRouteWithNullName_useSelectedRouteName() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
         // When the routing session does not have a name, and is a system session
         whenever(route.name).thenReturn(null)
         whenever(mr2.getSelectedRoutes(any())).thenReturn(listOf(selectedRoute))
@@ -725,101 +718,6 @@
         assertThat(data.showBroadcastButton).isFalse()
     }
 
-    // Duplicates of above tests with MEDIA_DEVICE_NAME_FIX enabled
-
-    @Test
-    fun loadMediaDataWithNullToken_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-    }
-
-    @Test
-    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        // Run and reset the executors and listeners so we only focus on new events.
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-
-        // Ensure we'll get device info when using the address
-        val fullMediaDevice = mock(MediaDevice::class.java)
-        val address = "fakeAddress"
-        val nameFromDevice = "nameFromDevice"
-        val iconFromDevice = mock(Drawable::class.java)
-        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
-        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
-        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)
-
-        // WHEN the about-to-connect device changes to non-null
-        val deviceCallback = captureCallback()
-        val nameFromParam = "nameFromParam"
-        val iconFromParam = mock(Drawable::class.java)
-        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN the about-to-connect device based on the address is returned
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(nameFromDevice)
-        assertThat(data.name).isNotEqualTo(nameFromParam)
-        assertThat(data.icon).isEqualTo(iconFromDevice)
-        assertThat(data.icon).isNotEqualTo(iconFromParam)
-    }
-
-    @Test
-    fun deviceNameFromMR2RouteInfo_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns a valid routing session
-        whenever(route.name).thenReturn(REMOTE_DEVICE_NAME)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN it uses the route name (instead of device name)
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
-    }
-
-    @Test
-    fun deviceDisabledWhenMR2ReturnsNullRouteInfo_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns null for routing session
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is disabled and name is set to null
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isNull()
-    }
-
-    @Test
-    fun mr2ReturnsNonSystemRouteWithNullName_useLocalDeviceName_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns a routing session that does not have a name
-        whenever(route.name).thenReturn(null)
-        whenever(route.isSystemSession).thenReturn(false)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is enabled and uses the current connected device name
-        val data = captureDeviceData(KEY)
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.enabled).isTrue()
-    }
-
-    // End duplicate tests
-
     private fun captureCallback(): LocalMediaManager.DeviceCallback {
         val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
         verify(lmm).registerCallback(captor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt
index 682b2d0..5eca8ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt
@@ -19,7 +19,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,8 +31,8 @@
 class QSTileConfigProviderTest : SysuiTestCase() {
 
     private val underTest =
-        QSTileConfigProviderImpl(
-            mapOf(VALID_SPEC.spec to QSTileConfigTestBuilder.build { tileSpec = VALID_SPEC })
+        createQSTileConfigProviderImpl(
+            mapOf(VALID_SPEC.spec to QSTileConfigTestBuilder.build { tileSpec = VALID_SPEC }),
         )
 
     @Test
@@ -43,13 +45,31 @@
         underTest.getConfig(INVALID_SPEC.spec)
     }
 
+    @Test
+    fun hasConfigReturnsTrueForValidSpec() {
+        assertThat(underTest.hasConfig(VALID_SPEC.spec)).isTrue()
+    }
+
+    @Test
+    fun hasConfigReturnsFalseForInvalidSpec() {
+        assertThat(underTest.hasConfig(INVALID_SPEC.spec)).isFalse()
+    }
+
     @Test(expected = IllegalArgumentException::class)
     fun validatesSpecUponCreation() {
-        QSTileConfigProviderImpl(
+        createQSTileConfigProviderImpl(
             mapOf(VALID_SPEC.spec to QSTileConfigTestBuilder.build { tileSpec = INVALID_SPEC })
         )
     }
 
+    private fun createQSTileConfigProviderImpl(
+        configs: Map<String, QSTileConfig>
+    ): QSTileConfigProviderImpl =
+        QSTileConfigProviderImpl(
+            configs,
+            mock<QsEventLogger>(),
+        )
+
     private companion object {
 
         val VALID_SPEC = TileSpec.create("valid_tile_spec")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
deleted file mode 100644
index d3b7daa..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ /dev/null
@@ -1,120 +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.qs.tiles.viewmodel
-
-import android.os.UserHandle
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
-import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
-import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
-import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
-import com.android.systemui.qs.tiles.base.logging.QSTileLogger
-import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-// TODO(b/299909368): Add more tests
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
-
-    @Mock private lateinit var qsTileLogger: QSTileLogger
-    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
-
-    private val fakeUserRepository = FakeUserRepository()
-    private val fakeQSTileDataInteractor = FakeQSTileDataInteractor<Any>()
-    private val fakeQSTileUserActionInteractor = FakeQSTileUserActionInteractor<Any>()
-    private val fakeDisabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
-    private val fakeFalsingManager = FalsingManagerFake()
-
-    private val testCoroutineDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testCoroutineDispatcher)
-
-    private lateinit var underTest: QSTileViewModel
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        underTest = createViewModel(testScope)
-    }
-
-    @Test
-    fun testDoesntListenStateUntilCreated() =
-        testScope.runTest {
-            assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
-
-            assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
-
-            underTest.state.launchIn(backgroundScope)
-            runCurrent()
-
-            assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty()
-            assertThat(fakeQSTileDataInteractor.dataRequests.first())
-                .isEqualTo(FakeQSTileDataInteractor.DataRequest(UserHandle.of(0)))
-        }
-
-    private fun createViewModel(
-        scope: TestScope,
-        config: QSTileConfig = TEST_QS_TILE_CONFIG,
-    ): QSTileViewModel =
-        QSTileViewModelImpl(
-            config,
-            { fakeQSTileUserActionInteractor },
-            { fakeQSTileDataInteractor },
-            {
-                object : QSTileDataToStateMapper<Any> {
-                    override fun map(config: QSTileConfig, data: Any): QSTileState =
-                        QSTileState.build(
-                            { Icon.Resource(0, ContentDescription.Resource(0)) },
-                            ""
-                        ) {}
-                }
-            },
-            fakeDisabledByPolicyInteractor,
-            fakeUserRepository,
-            fakeFalsingManager,
-            qsTileAnalytics,
-            qsTileLogger,
-            FakeSystemClock(),
-            testCoroutineDispatcher,
-            scope.backgroundScope,
-        )
-
-    private companion object {
-
-        val TEST_QS_TILE_CONFIG = QSTileConfigTestBuilder.build {}
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
new file mode 100644
index 0000000..3a0ebdb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.viewmodel
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSTileViewModelTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsTileLogger: QSTileLogger
+    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
+
+    private val tileConfig =
+        QSTileConfigTestBuilder.build { policy = QSTilePolicy.Restricted("test_restriction") }
+
+    private val userRepository = FakeUserRepository()
+    private val tileDataInteractor = FakeQSTileDataInteractor<String>()
+    private val tileUserActionInteractor = FakeQSTileUserActionInteractor<String>()
+    private val disabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
+    private val falsingManager = FalsingManagerFake()
+
+    private val testCoroutineDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testCoroutineDispatcher)
+
+    private lateinit var underTest: QSTileViewModel
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = createViewModel(testScope)
+    }
+
+    @Test
+    fun stateReceivedForTheData() =
+        testScope.runTest {
+            val testTileData = "test_tile_data"
+            val states = collectValues(underTest.state)
+            runCurrent()
+
+            tileDataInteractor.emitData(testTileData)
+            runCurrent()
+
+            assertThat(states()).isNotEmpty()
+            assertThat(states().first().label).isEqualTo(testTileData)
+            verify(qsTileLogger).logInitialRequest(eq(tileConfig.tileSpec))
+        }
+
+    @Test
+    fun doesntListenDataIfStateIsntListened() =
+        testScope.runTest {
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(0)
+
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(1)
+        }
+
+    @Test
+    fun doesntListenAvailabilityIfAvailabilityIsntListened() =
+        testScope.runTest {
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(0)
+
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(1)
+        }
+
+    @Test
+    fun doesntListedDataAfterDestroy() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.destroy()
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(0)
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(0)
+        }
+
+    @Test
+    fun forceUpdateTriggersData() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.forceUpdate()
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isInstanceOf(DataUpdateTrigger.ForceUpdate::class.java)
+            verify(qsTileLogger).logForceUpdate(eq(tileConfig.tileSpec))
+        }
+
+    @Test
+    fun userChangeUpdatesData() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onUserChanged(USER)
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataRequests.last())
+                .isEqualTo(FakeQSTileDataInteractor.DataRequest(USER))
+        }
+
+    @Test
+    fun userChangeUpdatesAvailability() =
+        testScope.runTest {
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onUserChanged(USER)
+            runCurrent()
+
+            assertThat(tileDataInteractor.availabilityRequests.last())
+                .isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
+        }
+
+    private fun createViewModel(
+        scope: TestScope,
+        config: QSTileConfig = tileConfig,
+    ): QSTileViewModel =
+        QSTileViewModelImpl(
+            config,
+            { tileUserActionInteractor },
+            { tileDataInteractor },
+            {
+                object : QSTileDataToStateMapper<String> {
+                    override fun map(config: QSTileConfig, data: String): QSTileState =
+                        QSTileState.build(
+                            { Icon.Resource(0, ContentDescription.Resource(0)) },
+                            data
+                        ) {}
+                }
+            },
+            disabledByPolicyInteractor,
+            userRepository,
+            falsingManager,
+            qsTileAnalytics,
+            qsTileLogger,
+            FakeSystemClock(),
+            testCoroutineDispatcher,
+            scope.backgroundScope,
+        )
+
+    private companion object {
+
+        val USER = UserHandle.of(1)!!
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
new file mode 100644
index 0000000..ea8acc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.viewmodel
+
+import androidx.test.filters.MediumTest
+import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** Tests all possible [QSTileUserAction]s. If you need */
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSTileViewModelUserInputTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsTileLogger: QSTileLogger
+    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
+
+    @Parameter lateinit var userAction: QSTileUserAction
+
+    private val tileConfig =
+        QSTileConfigTestBuilder.build { policy = QSTilePolicy.Restricted("test_restriction") }
+
+    private val userRepository = FakeUserRepository()
+    private val tileDataInteractor = FakeQSTileDataInteractor<String>()
+    private val tileUserActionInteractor = FakeQSTileUserActionInteractor<String>()
+    private val disabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
+    private val falsingManager = FalsingManagerFake()
+
+    private val testCoroutineDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testCoroutineDispatcher)
+
+    private lateinit var underTest: QSTileViewModel
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = createViewModel(testScope)
+    }
+
+    @Test
+    fun userInputTriggersData() =
+        testScope.runTest {
+            tileDataInteractor.emitData("initial_data")
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserAction(eq(userAction), eq(tileConfig.tileSpec), eq(true), eq(true))
+            verify(qsTileLogger)
+                .logUserActionPipeline(
+                    eq(tileConfig.tileSpec),
+                    eq(userAction),
+                    any(),
+                    eq("initial_data")
+                )
+            verify(qsTileAnalytics).trackUserAction(eq(tileConfig), eq(userAction))
+        }
+
+    @Test
+    fun disabledByPolicyUserInputIsSkipped() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            disabledByPolicyInteractor.policyResult =
+                DisabledByPolicyInteractor.PolicyResult.TileDisabled(
+                    RestrictedLockUtils.EnforcedAdmin()
+                )
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isNotInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserActionRejectedByPolicy(eq(userAction), eq(tileConfig.tileSpec))
+            verify(qsTileAnalytics, never()).trackUserAction(any(), any())
+        }
+
+    @Test
+    fun falsedUserInputIsSkipped() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            falsingManager.setFalseLongTap(true)
+            falsingManager.setFalseTap(true)
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isNotInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserActionRejectedByFalsing(eq(userAction), eq(tileConfig.tileSpec))
+            verify(qsTileAnalytics, never()).trackUserAction(any(), any())
+        }
+
+    @Test
+    fun userInputIsThrottled() =
+        testScope.runTest {
+            val inputCount = 100
+            underTest.state.launchIn(backgroundScope)
+
+            repeat(inputCount) { underTest.onActionPerformed(userAction) }
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.size).isLessThan(inputCount)
+        }
+
+    private fun createViewModel(scope: TestScope): QSTileViewModel =
+        QSTileViewModelImpl(
+            tileConfig,
+            { tileUserActionInteractor },
+            { tileDataInteractor },
+            {
+                object : QSTileDataToStateMapper<String> {
+                    override fun map(config: QSTileConfig, data: String): QSTileState =
+                        QSTileState.build(
+                            { Icon.Resource(0, ContentDescription.Resource(0)) },
+                            data
+                        ) {}
+                }
+            },
+            disabledByPolicyInteractor,
+            userRepository,
+            falsingManager,
+            qsTileAnalytics,
+            qsTileLogger,
+            FakeSystemClock(),
+            testCoroutineDispatcher,
+            scope.backgroundScope,
+        )
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters
+        fun data(): Iterable<QSTileUserAction> =
+            listOf(
+                QSTileUserAction.Click(null),
+                QSTileUserAction.LongClick(null),
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index e920687..20b19fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -158,6 +158,15 @@
         }
 
     @Test
+    fun updateLegacyLockscreenShadeTracking() =
+        testScope.runTest {
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(false)
+
+            underTest.setLegacyLockscreenShadeTracking(true)
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(true)
+        }
+
+    @Test
     fun updateLegacyQsTracking() =
         testScope.runTest {
             assertThat(underTest.legacyQsTracking.value).isEqualTo(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
index b8fe2f9..cb83e7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -20,10 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.smartspace.config.BcSmartspaceConfigProvider
-import com.android.systemui.util.mockito.whenever
-import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
@@ -45,16 +42,7 @@
     }
 
     @Test
-    fun isDefaultDateWeatherDisabled_flagIsTrue_returnsTrue() {
-        whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
-
+    fun isDefaultDateWeatherDisabled_returnsTrue() {
         assertTrue(configProvider.isDefaultDateWeatherDisabled)
     }
-
-    @Test
-    fun isDefaultDateWeatherDisabled_flagIsFalse_returnsFalse() {
-        whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
-
-        assertFalse(configProvider.isDefaultDateWeatherDisabled)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 9036f22..8440e00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -38,7 +38,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
@@ -205,10 +204,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        // Todo(b/261760571): flip the flag value here when feature is launched, and update relevant
-        //  tests.
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
-
         `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING))
                 .thenReturn(fakePrivateLockscreenSettingUri)
         `when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
@@ -260,17 +255,6 @@
         deviceProvisionedListener = deviceProvisionedCaptor.value
     }
 
-    @Test(expected = RuntimeException::class)
-    fun testBuildAndConnectWeatherView_throwsIfDecouplingDisabled() {
-        // GIVEN the feature flag is disabled
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
-
-        // WHEN we try to build the view
-        controller.buildAndConnectWeatherView(fakeParent)
-
-        // THEN an exception is thrown
-    }
-
     @Test
     fun testBuildAndConnectView_connectsOnlyAfterDeviceIsProvisioned() {
         // GIVEN an unprovisioned device and an attempt to connect
@@ -332,6 +316,8 @@
         clearInvocations(plugin)
 
         // WHEN the session is closed
+        controller.stateChangeListener.onViewDetachedFromWindow(dateSmartspaceView as View)
+        controller.stateChangeListener.onViewDetachedFromWindow(weatherSmartspaceView as View)
         controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
@@ -376,20 +362,6 @@
         configChangeListener.onThemeChanged()
 
         // We update the new text color to match the wallpaper color
-        verify(smartspaceView).setPrimaryTextColor(anyInt())
-    }
-
-    @Test
-    fun testThemeChange_ifDecouplingEnabled_updatesTextColor() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
-
-        // GIVEN a connected smartspace session
-        connectSession()
-
-        // WHEN the theme changes
-        configChangeListener.onThemeChanged()
-
-        // We update the new text color to match the wallpaper color
         verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
         verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
         verify(smartspaceView).setPrimaryTextColor(anyInt())
@@ -404,20 +376,6 @@
         statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
 
         // We pass that along to the view
-        verify(smartspaceView).setDozeAmount(0.7f)
-    }
-
-    @Test
-    fun testDozeAmountChange_ifDecouplingEnabled_updatesViews() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
-
-        // GIVEN a connected smartspace session
-        connectSession()
-
-        // WHEN the doze amount changes
-        statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
-
-        // We pass that along to the view
         verify(dateSmartspaceView).setDozeAmount(0.7f)
         verify(weatherSmartspaceView).setDozeAmount(0.7f)
         verify(smartspaceView).setDozeAmount(0.7f)
@@ -472,7 +430,7 @@
     }
 
     @Test
-    fun testAllTargetsAreFilteredExceptWeatherWhenNotificationsAreDisabled() {
+    fun testAllTargetsAreFilteredInclWeatherWhenNotificationsAreDisabled() {
         // GIVEN the active user doesn't allow any notifications on lockscreen
         setShowNotifications(userHandlePrimary, false)
         connectSession()
@@ -488,7 +446,7 @@
         sessionListener.onTargetsAvailable(targets)
 
         // THEN all non-sensitive content is still shown
-        verify(plugin).onTargetsAvailable(eq(listOf(targets[3])))
+        verify(plugin).onTargetsAvailable(emptyList())
     }
 
     @Test
@@ -519,8 +477,7 @@
     }
 
     @Test
-    fun testSessionListener_ifDecouplingEnabled_weatherTargetIsFilteredOut() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+    fun testSessionListener_weatherTargetIsFilteredOut() {
         connectSession()
 
         // WHEN we receive a list of targets
@@ -670,8 +627,7 @@
     }
 
     @Test
-    fun testSessionListener_ifDecouplingEnabled_weatherDataUpdates() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+    fun testSessionListener_weatherDataUpdates() {
         connectSession()
 
         clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
@@ -699,33 +655,6 @@
     }
 
     @Test
-    fun testSessionListener_ifDecouplingDisabled_weatherDataUpdates() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
-        connectSession()
-
-        clock.setCurrentTimeMillis(SMARTSPACE_TIME_JUST_RIGHT)
-        // WHEN we receive a list of targets
-        val targets = listOf(
-                makeWeatherTargetWithExtras(
-                        id = 1,
-                        userHandle = userHandlePrimary,
-                        description = "Sunny",
-                        state = WeatherData.WeatherStateIcon.SUNNY.id,
-                        temperature = "32",
-                        useCelsius = false),
-                makeTarget(2, userHandlePrimary, isSensitive = true)
-        )
-
-        sessionListener.onTargetsAvailable(targets)
-
-        verify(keyguardUpdateMonitor).sendWeatherData(argThat { w ->
-            w.description == "Sunny" &&
-                    w.state == WeatherData.WeatherStateIcon.SUNNY &&
-                    w.temperature == 32 && !w.useCelsius
-        })
-    }
-
-    @Test
     fun testSettingsAreReloaded() {
         // GIVEN a connected session where the privacy settings later flip to false
         connectSession()
@@ -781,6 +710,8 @@
         connectSession()
 
         // WHEN we are told to cleanup
+        controller.stateChangeListener.onViewDetachedFromWindow(dateSmartspaceView as View)
+        controller.stateChangeListener.onViewDetachedFromWindow(weatherSmartspaceView as View)
         controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
@@ -816,16 +747,6 @@
     }
 
     @Test
-    fun testWeatherViewUsesSameSession() {
-        `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
-        // GIVEN a connected session
-        connectSession()
-
-        // No checks is needed here, since connectSession() already checks internally that
-        // createSmartspaceSession is invoked only once.
-    }
-
-    @Test
     fun testViewGetInitializedWithBypassEnabledState() {
         // GIVEN keyguard bypass is enabled.
         `when`(keyguardBypassController.bypassEnabled).thenReturn(true)
@@ -853,31 +774,29 @@
     }
 
     private fun connectSession() {
-        if (controller.isDateWeatherDecoupled()) {
-            val dateView = controller.buildAndConnectDateView(fakeParent)
-            dateSmartspaceView = dateView as SmartspaceView
-            fakeParent.addView(dateView)
-            controller.stateChangeListener.onViewAttachedToWindow(dateView)
+        val dateView = controller.buildAndConnectDateView(fakeParent)
+        dateSmartspaceView = dateView as SmartspaceView
+        fakeParent.addView(dateView)
+        controller.stateChangeListener.onViewAttachedToWindow(dateView)
 
-            verify(dateSmartspaceView).setUiSurface(
-                    BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
-            verify(dateSmartspaceView).registerDataProvider(datePlugin)
+        verify(dateSmartspaceView).setUiSurface(
+                BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+        verify(dateSmartspaceView).registerDataProvider(datePlugin)
 
-            verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
-            verify(dateSmartspaceView).setDozeAmount(0.5f)
+        verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(dateSmartspaceView).setDozeAmount(0.5f)
 
-            val weatherView = controller.buildAndConnectWeatherView(fakeParent)
-            weatherSmartspaceView = weatherView as SmartspaceView
-            fakeParent.addView(weatherView)
-            controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+        val weatherView = controller.buildAndConnectWeatherView(fakeParent)
+        weatherSmartspaceView = weatherView as SmartspaceView
+        fakeParent.addView(weatherView)
+        controller.stateChangeListener.onViewAttachedToWindow(weatherView)
 
-            verify(weatherSmartspaceView).setUiSurface(
-                    BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
-            verify(weatherSmartspaceView).registerDataProvider(weatherPlugin)
+        verify(weatherSmartspaceView).setUiSurface(
+                BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+        verify(weatherSmartspaceView).registerDataProvider(weatherPlugin)
 
-            verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
-            verify(weatherSmartspaceView).setDozeAmount(0.5f)
-        }
+        verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+        verify(weatherSmartspaceView).setDozeAmount(0.5f)
 
         val view = controller.buildAndConnectView(fakeParent)
         smartspaceView = view as SmartspaceView
@@ -918,10 +837,8 @@
         verify(smartspaceView).setPrimaryTextColor(anyInt())
         verify(smartspaceView).setDozeAmount(0.5f)
 
-        if (controller.isDateWeatherDecoupled()) {
-            clearInvocations(dateSmartspaceView)
-            clearInvocations(weatherSmartspaceView)
-        }
+        clearInvocations(dateSmartspaceView)
+        clearInvocations(weatherSmartspaceView)
         clearInvocations(smartspaceView)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index 1a04a3e..87e9735 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -389,4 +389,31 @@
             assertThat(isolatedIcon?.value?.notifKey).isEqualTo("notif1")
             assertThat(isolatedIcon?.isAnimating).isFalse()
         }
+
+    @Test
+    fun isolatedIcon_updateWhenIconDataChanges() =
+        testComponent.runTest {
+            val icon: Icon = mock()
+            val isolatedIcon by collectLastValue(underTest.isolatedIcon)
+            runCurrent()
+
+            headsUpViewStateRepository.isolatedNotification.value = "notif1"
+            runCurrent()
+
+            activeNotificationsRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply {
+                        addIndividualNotif(
+                            activeNotificationModel(
+                                key = "notif1",
+                                groupKey = "group",
+                                statusBarIcon = icon
+                            )
+                        )
+                    }
+                    .build()
+            runCurrent()
+
+            assertThat(isolatedIcon?.value?.notifKey).isEqualTo("notif1")
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 6203531..e91d6d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -83,6 +83,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -906,6 +907,20 @@
         assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset);
     }
 
+    @Test
+    public void testSetMaxDisplayedNotifications_notifiesListeners() {
+        ExpandableView.OnHeightChangedListener listener =
+                mock(ExpandableView.OnHeightChangedListener.class);
+        Runnable runnable = mock(Runnable.class);
+        mStackScroller.setOnHeightChangedListener(listener);
+        mStackScroller.setOnHeightChangedRunnable(runnable);
+
+        mStackScroller.setMaxDisplayedNotifications(50);
+
+        verify(listener).onHeightChanged(mNotificationShelf, false);
+        verify(runnable).run();
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 22553df..db8f217 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -40,13 +40,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -84,25 +79,13 @@
         }
     }
 
-    private val notificationStackSizeCalculator: NotificationStackSizeCalculator = mock()
-    private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController =
-        mock {
-            whenever(view).thenReturn(mock())
-            whenever(shelfHeight).thenReturn(0)
-        }
-
     private val testComponent: TestComponent =
         DaggerSharedNotificationContainerViewModelTest_TestComponent.factory()
             .create(
                 test = this,
                 featureFlags =
                     FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
-                mocks =
-                    TestMocksModule(
-                        notificationStackSizeCalculator = notificationStackSizeCalculator,
-                        notificationStackScrollLayoutController =
-                            notificationStackScrollLayoutController,
-                    )
+                mocks = TestMocksModule(),
             )
 
     @Test
@@ -336,17 +319,9 @@
     @Test
     fun maxNotificationsOnLockscreen() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
 
             showLockscreen()
 
@@ -356,21 +331,52 @@
                 SharedNotificationContainerPosition(top = 1f, bottom = 2f)
 
             assertThat(maxNotifications).isEqualTo(10)
+
+            // Also updates when directly requested (as it would from NotificationStackScrollLayout)
+            notificationCount = 25
+            sharedNotificationContainerInteractor.notificationStackChanged()
+            assertThat(maxNotifications).isEqualTo(25)
+        }
+
+    @Test
+    fun maxNotificationsOnLockscreen_DoesNotUpdateWhenUserInteracting() =
+        testComponent.runTest {
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
+
+            showLockscreen()
+
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            configurationRepository.onAnyConfigurationChange()
+            keyguardInteractor.sharedNotificationContainerPosition.value =
+                SharedNotificationContainerPosition(top = 1f, bottom = 2f)
+
+            assertThat(maxNotifications).isEqualTo(10)
+
+            // Shade expanding... still 10
+            shadeRepository.setLockscreenShadeExpansion(0.5f)
+            assertThat(maxNotifications).isEqualTo(10)
+
+            notificationCount = 25
+
+            // When shade is expanding by user interaction
+            shadeRepository.setLegacyLockscreenShadeTracking(true)
+
+            // Should still be 10, since the user is interacting
+            assertThat(maxNotifications).isEqualTo(10)
+
+            shadeRepository.setLegacyLockscreenShadeTracking(false)
+            shadeRepository.setLockscreenShadeExpansion(0f)
+
+            // Stopped tracking, show 25
+            assertThat(maxNotifications).isEqualTo(25)
         }
 
     @Test
     fun maxNotificationsOnShade() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            val maxNotifications by collectLastValue(underTest.getMaxNotifications { 10 })
 
             // Show lockscreen with shade expanded
             showLockscreenWithShadeExpanded()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index d1b9b8a..0b87fe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -58,6 +58,7 @@
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -703,6 +704,7 @@
                 mKeyguardStateController,
                 mShadeViewController,
                 mStatusBarStateController,
+                mock(StatusBarIconViewBindingFailureTracker.class),
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 7456e00..8c823b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -20,7 +20,6 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
@@ -68,7 +67,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialogController;
@@ -149,14 +147,13 @@
         }
     };
 
-    private FakeFeatureFlags mFeatureFlags;
     private int mLongestHideShowAnimationDuration = 250;
     private FakeSettings mSecureSettings;
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
 
-    @Before
+   @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
@@ -179,8 +176,6 @@
 
         mConfigurationController = new FakeConfigurationController();
 
-        mFeatureFlags = new FakeFeatureFlags();
-
         mSecureSettings = new FakeSettings();
 
         when(mLazySecureSettings.get()).thenReturn(mSecureSettings);
@@ -200,7 +195,6 @@
                 mPostureController,
                 mTestableLooper.getLooper(),
                 mDumpManager,
-                mFeatureFlags,
                 mLazySecureSettings);
         mDialog.init(0, null);
         State state = createShellState();
@@ -328,7 +322,6 @@
 
     @Test
     public void testVibrateOnRingerChangedToVibrate() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialSilentState = new State();
         initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
 
@@ -349,30 +342,7 @@
     }
 
     @Test
-    public void testControllerDoesNotVibrateOnRingerChangedToVibrate_OnewayAPI_On() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialSilentState = new State();
-        initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
-
-        final State vibrateState = new State();
-        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-
-        // change ringer to silent
-        mDialog.onStateChangedH(initialSilentState);
-
-        // expected: shouldn't call vibrate yet
-        verify(mVolumeDialogController, never()).vibrate(any());
-
-        // changed ringer to vibrate
-        mDialog.onStateChangedH(vibrateState);
-
-        // expected: vibrate method of controller is not used
-        verify(mVolumeDialogController, never()).vibrate(any());
-    }
-
-    @Test
     public void testNoVibrateOnRingerInitialization() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = -1;
 
@@ -390,29 +360,9 @@
     }
 
     @Test
-    public void testControllerDoesNotVibrateOnRingerInitialization_OnewayAPI_On() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = -1;
-
-        // ringer not initialized yet:
-        mDialog.onStateChangedH(initialUnsetState);
-
-        final State vibrateState = new State();
-        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-
-        // changed ringer to vibrate
-        mDialog.onStateChangedH(vibrateState);
-
-        // shouldn't call vibrate on the controller either
-        verify(mVolumeDialogController, never()).vibrate(any());
-    }
-
-    @Test
     public void testSelectVibrateFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -426,27 +376,9 @@
     }
 
     @Test
-    public void testSelectVibrateFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerVibrate.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                AudioManager.RINGER_MODE_VIBRATE, false);
-    }
-
-    @Test
     public void testSelectMuteFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -460,27 +392,9 @@
     }
 
     @Test
-    public void testSelectMuteFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerMute.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                AudioManager.RINGER_MODE_SILENT, false);
-    }
-
-    @Test
     public void testSelectNormalFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
         mDialog.onStateChangedH(initialUnsetState);
@@ -493,23 +407,6 @@
                 AudioManager.RINGER_MODE_NORMAL, false);
     }
 
-    @Test
-    public void testSelectNormalFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerNormal.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                RINGER_MODE_NORMAL, false);
-    }
-
     /**
      * Ideally we would look at the ringer ImageView and check its assigned drawable id, but that
      * API does not exist. So we do the next best thing; we check the cached icon id.
@@ -682,7 +579,6 @@
 
         State state = createShellState();
         state.ringerModeInternal = ringerMode;
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         mDialog.onStateChangedH(state);
 
         mDialog.show(SHOW_REASON_UNKNOWN);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 6e9363b..af1930e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -60,6 +60,10 @@
 
     override val minPatternLength: Int = 4
 
+    private val _isPinEnhancedPrivacyEnabled = MutableStateFlow(false)
+    override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
+        _isPinEnhancedPrivacyEnabled.asStateFlow()
+
     private var failedAttemptCount = 0
     private var throttlingEndTimestamp = 0L
     private var credentialOverride: List<Any>? = null
@@ -138,6 +142,10 @@
         }
     }
 
+    fun setPinEnhancedPrivacyEnabled(isEnabled: Boolean) {
+        _isPinEnhancedPrivacyEnabled.value = isEnabled
+    }
+
     private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
         return when (val credentialType = getCurrentCredentialType(securityMode)) {
             LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN
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..005cac4 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
@@ -19,10 +19,15 @@
 import android.hardware.biometrics.SensorLocationInternal
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
+@SysUISingleton
+class FakeFingerprintPropertyRepository @Inject constructor() : FingerprintPropertyRepository {
 
     private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
     override val sensorId = _sensorId.asStateFlow()
@@ -50,4 +55,29 @@
         _sensorType.value = sensorType
         _sensorLocations.value = sensorLocations
     }
+
+    /** setProperties as if the device supports UDFPS_OPTICAL. */
+    fun supportsUdfps() {
+        setProperties(
+            sensorId = 0,
+            strength = SensorStrength.STRONG,
+            sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+            sensorLocations = emptyMap(),
+        )
+    }
+
+    /** setProperties as if the device supports the rear fingerprint sensor. */
+    fun supportsRearFps() {
+        setProperties(
+            sensorId = 0,
+            strength = SensorStrength.STRONG,
+            sensorType = FingerprintSensorType.REAR,
+            sensorLocations = emptyMap(),
+        )
+    }
+}
+
+@Module
+interface FakeFingerprintPropertyRepositoryModule {
+    @Binds fun bindFake(fake: FakeFingerprintPropertyRepository): FingerprintPropertyRepository
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
index 44286b7..8ff04a63 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
@@ -15,17 +15,23 @@
 
 package com.android.systemui.deviceentry.data
 
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepositoryModule
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeTrustRepositoryModule
 import dagger.Module
 
 @Module(
     includes =
         [
+            FakeBiometricSettingsRepositoryModule::class,
             FakeDeviceEntryRepositoryModule::class,
-            FakeTrustRepositoryModule::class,
             FakeDeviceEntryFaceAuthRepositoryModule::class,
+            FakeDeviceEntryFingerprintAuthRepositoryModule::class,
+            FakeFingerprintPropertyRepositoryModule::class,
+            FakeTrustRepositoryModule::class,
         ]
 )
 object FakeDeviceEntryDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index 85261123..df31a12 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -18,13 +18,18 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 
-class FakeBiometricSettingsRepository : BiometricSettingsRepository {
+@SysUISingleton
+class FakeBiometricSettingsRepository @Inject constructor() : BiometricSettingsRepository {
     private val _isFingerprintEnrolledAndEnabled = MutableStateFlow(false)
     override val isFingerprintEnrolledAndEnabled: StateFlow<Boolean>
         get() = _isFingerprintEnrolledAndEnabled
@@ -97,3 +102,8 @@
         }
     }
 }
+
+@Module
+interface FakeBiometricSettingsRepositoryModule {
+    @Binds fun bindFake(fake: FakeBiometricSettingsRepository): BiometricSettingsRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
index 38791ca..c9160ef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -17,14 +17,20 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 
-class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository {
+@SysUISingleton
+class FakeDeviceEntryFingerprintAuthRepository @Inject constructor() :
+    DeviceEntryFingerprintAuthRepository {
     private val _isLockedOut = MutableStateFlow(false)
     override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow()
     fun setLockedOut(lockedOut: Boolean) {
@@ -52,3 +58,11 @@
         _authenticationStatus.value = status
     }
 }
+
+@Module
+interface FakeDeviceEntryFingerprintAuthRepositoryModule {
+    @Binds
+    fun bindFake(
+        fake: FakeDeviceEntryFingerprintAuthRepository
+    ): DeviceEntryFingerprintAuthRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index b90ad8c..3674244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -151,6 +151,17 @@
         _transitions.emit(step)
     }
 
+    suspend fun sendTransitionSteps(
+        steps: List<TransitionStep>,
+        testScope: TestScope,
+        validateStep: Boolean = true
+    ) {
+        steps.forEach {
+            sendTransitionStep(it, validateStep = validateStep)
+            testScope.testScheduler.runCurrent()
+        }
+    }
+
     override fun startTransition(info: TransitionInfo): UUID? {
         return if (info.animator == null) UUID.randomUUID() else null
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
index 1efa74b..62765d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
@@ -20,7 +20,6 @@
 
 class FakeDisabledByPolicyInteractor : DisabledByPolicyInteractor {
 
-    var handleResult: Boolean = false
     var policyResult: DisabledByPolicyInteractor.PolicyResult =
         DisabledByPolicyInteractor.PolicyResult.TileEnabled
 
@@ -31,5 +30,9 @@
 
     override fun handlePolicyResult(
         policyResult: DisabledByPolicyInteractor.PolicyResult
-    ): Boolean = handleResult
+    ): Boolean =
+        when (policyResult) {
+            is DisabledByPolicyInteractor.PolicyResult.TileEnabled -> false
+            is DisabledByPolicyInteractor.PolicyResult.TileDisabled -> true
+        }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
index 2b3330f..3fcf8a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
@@ -17,16 +17,21 @@
 package com.android.systemui.qs.tiles.base.interactor
 
 import android.os.UserHandle
-import javax.annotation.CheckReturnValue
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.flatMapLatest
 
-class FakeQSTileDataInteractor<T>(
-    private val dataFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = Int.MAX_VALUE),
-    private val availabilityFlow: MutableSharedFlow<Boolean> =
-        MutableSharedFlow(replay = Int.MAX_VALUE),
-) : QSTileDataInteractor<T> {
+class FakeQSTileDataInteractor<T> : QSTileDataInteractor<T> {
+
+    private val dataFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = 1)
+    val dataSubscriptionCount
+        get() = dataFlow.subscriptionCount
+    private val availabilityFlow: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
+    val availabilitySubscriptionCount
+        get() = availabilityFlow.subscriptionCount
+
+    private val mutableTriggers = mutableListOf<DataUpdateTrigger>()
+    val triggers: List<DataUpdateTrigger> = mutableTriggers
 
     private val mutableDataRequests = mutableListOf<DataRequest>()
     val dataRequests: List<DataRequest> = mutableDataRequests
@@ -34,14 +39,17 @@
     private val mutableAvailabilityRequests = mutableListOf<AvailabilityRequest>()
     val availabilityRequests: List<AvailabilityRequest> = mutableAvailabilityRequests
 
-    @CheckReturnValue fun emitData(data: T): Boolean = dataFlow.tryEmit(data)
+    suspend fun emitData(data: T): Unit = dataFlow.emit(data)
 
     fun tryEmitAvailability(isAvailable: Boolean): Boolean = availabilityFlow.tryEmit(isAvailable)
     suspend fun emitAvailability(isAvailable: Boolean) = availabilityFlow.emit(isAvailable)
 
     override fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<T> {
         mutableDataRequests.add(DataRequest(user))
-        return triggers.flatMapLatest { dataFlow }
+        return triggers.flatMapLatest {
+            mutableTriggers.add(it)
+            dataFlow
+        }
     }
 
     override fun availability(user: UserHandle): Flow<Boolean> {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt
index de72a7d..d231d63 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt
@@ -24,6 +24,8 @@
 
     override fun getConfig(tileSpec: String): QSTileConfig = configs.getValue(tileSpec)
 
+    override fun hasConfig(tileSpec: String): Boolean = configs.containsKey(tileSpec)
+
     fun putConfig(tileSpec: TileSpec, config: QSTileConfig) {
         configs[tileSpec.spec] = config
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 800593f..02318ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -59,6 +59,8 @@
     private val _legacyIsQsExpanded = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
         _legacyIsQsExpanded.value = legacyIsQsExpanded
@@ -81,6 +83,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     fun setShadeModel(model: ShadeModel) {
         _shadeModel.value = model
     }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ec12d21..fc4ed1d 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -28,9 +28,8 @@
     name: "ravenwood-junit",
     srcs: ["junit-src/**/*.java"],
     libs: [
+        "framework-minus-apex.ravenwood",
         "junit",
     ],
-    sdk_version: "core_current",
-    host_supported: true,
     visibility: ["//visibility:public"],
 }
diff --git a/ravenwood/README-ravenwood+mockito.md b/ravenwood/README-ravenwood+mockito.md
new file mode 100644
index 0000000..6adb6144
--- /dev/null
+++ b/ravenwood/README-ravenwood+mockito.md
@@ -0,0 +1,24 @@
+# Ravenwood and Mockito
+
+Last update: 2023-11-13
+
+- As of 2023-11-13, `external/mockito` is based on version 2.x.
+- Mockito didn't support static mocking before 3.4.0.
+  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
+
+- Latest Mockito is 5.*. According to https://github.com/mockito/mockito:
+  `Mockito 3 does not introduce any breaking API changes, but now requires Java 8 over Java 6 for Mockito 2. Mockito 4 removes deprecated API. Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.`
+
+- Mockito now supports Android natively.
+  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.1
+  - But it's unclear at this point to omakoto@ how the `mockito-android` module is built.
+
+- Potential plan:
+  - Ideal option:
+    - If we can update `external/mockito`, that'd be great, but it may not work because
+      Mockito has removed the deprecated APIs.
+  - Second option:
+    - Import the latest mockito as `external/mockito-new`, and require ravenwood
+      to use this one.
+    - The latest mockito needs be exposed to all of 1) device tests, 2) host tests, and 3) ravenwood tests.
+    - This probably will require the latest `bytebuddy` and `objenesis`.
\ No newline at end of file
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodClassLoadHook.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodClassLoadHook.java
similarity index 96%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodClassLoadHook.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodClassLoadHook.java
index 76964a7..7dc197e 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodClassLoadHook.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodClassLoadHook.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.TYPE;
 
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodKeep.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeep.java
similarity index 96%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodKeep.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeep.java
index ddf65dc..1d31579 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodKeep.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeep.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.FIELD;
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodWholeClassKeep.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeepWholeClass.java
similarity index 93%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodWholeClassKeep.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeepWholeClass.java
index d7ef7f5..d2c77c1 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodWholeClassKeep.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodKeepWholeClass.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.FIELD;
@@ -35,5 +35,5 @@
  */
 @Target({TYPE, FIELD, METHOD, CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodWholeClassKeep {
+public @interface RavenwoodKeepWholeClass {
 }
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
similarity index 96%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
index 8cdc1ff..4b9cf85 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.TYPE;
 
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodRemove.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRemove.java
similarity index 96%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodRemove.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRemove.java
index 759c918..6727327 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodRemove.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRemove.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.FIELD;
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
similarity index 83%
copy from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java
copy to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
index 8cdc1ff..a920f63 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -29,8 +29,7 @@
  *
  * @hide
  */
-@Target({TYPE})
+@Target({METHOD})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
-    String value();
+public @interface RavenwoodReplace {
 }
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodThrow.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodThrow.java
similarity index 96%
rename from ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodThrow.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodThrow.java
index de3dd04..a234a9b 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodThrow.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodThrow.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotations;
+package android.ravenwood.annotation;
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
diff --git a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodSubstitute.java b/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodSubstitute.java
deleted file mode 100644
index 5a0a8f4..0000000
--- a/ravenwood/annotations-src/android/ravenwood/annotations/RavenwoodSubstitute.java
+++ /dev/null
@@ -1,38 +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 android.ravenwood.annotations;
-
-import static java.lang.annotation.ElementType.METHOD;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
- * QUESTIONS ABOUT IT.
- *
- * TODO: Javadoc
- *
- * @hide
- */
-@Target({METHOD})
-@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodSubstitute {
-    // TODO We should add "_host" as default. We're not doing it yet, because extractign the default
-    // value with ASM doesn't seem trivial. (? not sure.)
-    String suffix();
-}
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 48c0a2d..692d598 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -76,21 +76,13 @@
 class android.util.UtilConfig stubclass
 
 # Internals
-class com.android.internal.util.ArrayUtils stubclass
-    method newUnpaddedByteArray (I)[B @newUnpaddedByteArray$ravenwood
-    method newUnpaddedCharArray (I)[C @newUnpaddedCharArray$ravenwood
-    method newUnpaddedIntArray (I)[I @newUnpaddedIntArray$ravenwood
-    method newUnpaddedBooleanArray (I)[Z @newUnpaddedBooleanArray$ravenwood
-    method newUnpaddedLongArray (I)[J @newUnpaddedLongArray$ravenwood
-    method newUnpaddedFloatArray (I)[F @newUnpaddedFloatArray$ravenwood
-    method newUnpaddedObjectArray (I)[Ljava/lang/Object; @newUnpaddedObjectArray$ravenwood
-    method newUnpaddedArray (Ljava/lang/Class;I)[Ljava/lang/Object; @newUnpaddedArray$ravenwood
-
 class com.android.internal.util.GrowingArrayUtils stubclass
 class com.android.internal.util.LineBreakBufferedWriter stubclass
 class com.android.internal.util.Preconditions stubclass
 class com.android.internal.util.StringPool stubclass
 
+class com.android.internal.os.SomeArgs stubclass
+
 # Parcel
 class android.os.Parcel stubclass
     method writeException (Ljava/lang/Exception;)V @writeException$ravenwood
@@ -102,14 +94,16 @@
 class android.os.BadParcelableException stubclass
 class android.os.BadTypeParcelableException stubclass
 
-# Binder: just enough to construct, no further functionality
-class android.os.Binder stub
-    method <init> ()V stub
-    method <init> (Ljava/lang/String;)V stub
-    method isDirectlyHandlingTransaction ()Z stub
-    method isDirectlyHandlingTransactionNative ()Z @isDirectlyHandlingTransactionNative$ravenwood
-    method getNativeBBinderHolder ()J @getNativeBBinderHolder$ravenwood
+# Binder
+class android.os.DeadObjectException stubclass
+class android.os.DeadSystemException stubclass
+class android.os.RemoteException stubclass
+class android.os.TransactionTooLargeException stubclass
 
 # Containers
 class android.os.BaseBundle stubclass
 class android.os.Bundle stubclass
+
+# Misc
+class android.os.PatternMatcher stubclass
+class android.os.ParcelUuid stubclass
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index a6b3f66..bffd0cd 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -16,6 +16,7 @@
 
 package android.platform.test.ravenwood;
 
+import android.os.Process;
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 
 import org.junit.Assume;
@@ -23,6 +24,8 @@
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * THIS RULE IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
  * QUESTIONS ABOUT IT.
@@ -30,20 +33,84 @@
  * @hide
  */
 public class RavenwoodRule implements TestRule {
+    private static AtomicInteger sNextPid = new AtomicInteger(100);
+
+    /**
+     * Unless the test author requests differently, run as "nobody", and give each collection of
+     * tests its own unique PID.
+     */
+    private int mUid = android.os.Process.NOBODY_UID;
+    private int mPid = sNextPid.getAndIncrement();
+
+    public RavenwoodRule() {
+    }
+
+    public static class Builder {
+        private RavenwoodRule mRule = new RavenwoodRule();
+
+        public Builder() {
+        }
+
+        /**
+         * Configure the identity of this process to be the system UID for the duration of the
+         * test. Has no effect under non-Ravenwood environments.
+         */
+        public Builder setProcessSystem() {
+            mRule.mUid = android.os.Process.SYSTEM_UID;
+            return this;
+        }
+
+        /**
+         * Configure the identity of this process to be an app UID for the duration of the
+         * test. Has no effect under non-Ravenwood environments.
+         */
+        public Builder setProcessApp() {
+            mRule.mUid = android.os.Process.FIRST_APPLICATION_UID;
+            return this;
+        }
+
+        public RavenwoodRule build() {
+            return mRule;
+        }
+    }
+
+    /**
+     * Return if the current process is running under a Ravenwood test environment.
+     */
     public boolean isUnderRavenwood() {
         // TODO: give ourselves a better environment signal
         return System.getProperty("java.class.path").contains("ravenwood");
     }
 
+    private void init() {
+        android.os.Process.init$ravenwood(mUid, mPid);
+        android.os.Binder.init$ravenwood();
+    }
+
+    private void reset() {
+        android.os.Process.reset$ravenwood();
+        android.os.Binder.reset$ravenwood();
+    }
+
     @Override
     public Statement apply(Statement base, Description description) {
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
+                final boolean isUnderRavenwood = isUnderRavenwood();
                 if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) {
-                    Assume.assumeFalse(isUnderRavenwood());
+                    Assume.assumeFalse(isUnderRavenwood);
                 }
-                base.evaluate();
+                if (isUnderRavenwood) {
+                    init();
+                }
+                try {
+                    base.evaluate();
+                } finally {
+                    if (isUnderRavenwood) {
+                        reset();
+                    }
+                }
             }
         };
     }
diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp
new file mode 100644
index 0000000..4135022
--- /dev/null
+++ b/ravenwood/mockito/Android.bp
@@ -0,0 +1,72 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+// Ravenwood tests run on the hostside, so we need mockit of the host variant.
+// But we need to use it in modules of the android variant, so we "wash" the variant with it.
+java_host_for_device {
+    name: "mockito_ravenwood",
+    libs: [
+        "mockito",
+        "objenesis",
+    ],
+}
+
+android_ravenwood_test {
+    name: "RavenwoodMockitoTest",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    static_libs: [
+        "junit",
+        "truth",
+
+        "mockito_ravenwood",
+    ],
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+    auto_gen_config: true,
+}
+
+android_test {
+    name: "RavenwoodMockitoTest_device",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    static_libs: [
+        "junit",
+        "truth",
+
+        "androidx.test.rules",
+
+        "ravenwood-junit",
+
+        "mockito-target-extended-minus-junit4",
+    ],
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+    jni_libs: [
+        // Required by mockito
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/ravenwood/mockito/AndroidManifest.xml b/ravenwood/mockito/AndroidManifest.xml
new file mode 100644
index 0000000..15f0a29
--- /dev/null
+++ b/ravenwood/mockito/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.mockitotest">
+
+    <application android:debuggable="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.ravenwood.mockitotest"
+        />
+</manifest>
diff --git a/ravenwood/mockito/AndroidTest.xml b/ravenwood/mockito/AndroidTest.xml
new file mode 100644
index 0000000..96bc275
--- /dev/null
+++ b/ravenwood/mockito/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<configuration description="Runs Frameworks Services Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="RavenwoodMockitoTest_device.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="FrameworksMockingServicesTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.ravenwood.mockitotest" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
new file mode 100644
index 0000000..36fa3dd
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+public class RavenwoodMockitoTest {
+    @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+
+// Use this to mock static methods, which isn't supported by mockito 2.
+// Mockito supports static mocking since 3.4.0:
+// See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
+
+//    private MockitoSession mMockingSession;
+//
+//    @Before
+//    public void setUp() {
+//        mMockingSession = mockitoSession()
+//                .strictness(Strictness.LENIENT)
+//                .mockStatic(RavenwoodMockitoTest.class)
+//                .startMocking();
+//    }
+//
+//    @After
+//    public void tearDown() {
+//        if (mMockingSession != null) {
+//            mMockingSession.finishMocking();
+//        }
+//    }
+
+    @Test
+    public void testMockJdkClass() {
+        Process object = mock(Process.class);
+
+        when(object.exitValue()).thenReturn(42);
+
+        assertThat(object.exitValue()).isEqualTo(42);
+    }
+
+    /*
+ - Intent can't be mocked because of the dependency to `org.xmlpull.v1.XmlPullParser`.
+   (The error says "Mockito can only mock non-private & non-final classes", but that's likely a
+   red-herring.)
+
+STACKTRACE:
+org.mockito.exceptions.base.MockitoException:
+Mockito cannot mock this class: class android.content.Intent.
+
+  :
+
+Underlying exception : java.lang.IllegalArgumentException: Could not create type
+    at com.android.ravenwood.mockito.RavenwoodMockitoTest.testMockAndroidClass1
+    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+
+  :
+
+Caused by: java.lang.ClassNotFoundException: org.xmlpull.v1.XmlPullParser
+    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
+    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
+    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
+    ... 54 more
+     */
+    @Test
+    @IgnoreUnderRavenwood
+    public void testMockAndroidClass1() {
+        Intent object = mock(Intent.class);
+
+        when(object.getAction()).thenReturn("ACTION_RAVENWOOD");
+
+        assertThat(object.getAction()).isEqualTo("ACTION_RAVENWOOD");
+    }
+
+    @Test
+    public void testMockAndroidClass2() {
+        Context object = mock(Context.class);
+
+        when(object.getPackageName()).thenReturn("android");
+
+        assertThat(object.getPackageName()).isEqualTo("android");
+    }
+}
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 0811f90..776a19a 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -1,2 +1,9 @@
 # Only classes listed here can use the Ravenwood annotations.
 
+com.android.internal.util.ArrayUtils
+
+android.os.Binder
+android.os.Binder$IdentitySupplier
+android.os.IBinder
+android.os.Process
+android.os.SystemClock
diff --git a/ravenwood/ravenwood-standard-options.txt b/ravenwood/ravenwood-standard-options.txt
index 6e1384f..4b07ef6 100644
--- a/ravenwood/ravenwood-standard-options.txt
+++ b/ravenwood/ravenwood-standard-options.txt
@@ -16,22 +16,22 @@
 # Standard annotations.
 # Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
 --keep-annotation
-    android.ravenwood.annotations.RavenwoodKeep
+    android.ravenwood.annotation.RavenwoodKeep
 
 --keep-class-annotation
-    android.ravenwood.annotations.RavenwoodWholeClassKeep
+    android.ravenwood.annotation.RavenwoodKeepWholeClass
 
 --throw-annotation
-    android.ravenwood.annotations.RavenwoodThrow
+    android.ravenwood.annotation.RavenwoodThrow
 
 --remove-annotation
-    android.ravenwood.annotations.RavenwoodRemove
+    android.ravenwood.annotation.RavenwoodRemove
 
 --substitute-annotation
-    android.ravenwood.annotations.RavenwoodSubstitute
+    android.ravenwood.annotation.RavenwoodReplace
 
 --native-substitute-annotation
-    android.ravenwood.annotations.RavenwoodNativeSubstitutionClass
+    android.ravenwood.annotation.RavenwoodNativeSubstitutionClass
 
 --class-load-hook-annotation
-    android.ravenwood.annotations.RavenwoodClassLoadHook
+    android.ravenwood.annotation.RavenwoodClassLoadHook
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 83143a4..5295ec8 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -1,3 +1,5 @@
+# Bug component: 1171888
+
 set noparent
 
 ogunwale@google.com
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 92af68b..85b3c9a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -441,10 +441,8 @@
                                 + " is not the owner of the supplied VirtualDevice");
             }
 
-            int displayId = virtualDeviceImpl.createVirtualDisplay(virtualDisplayConfig, callback,
-                    packageName);
-            mLocalService.onVirtualDisplayCreated(displayId);
-            return displayId;
+            return virtualDeviceImpl.createVirtualDisplay(
+                    virtualDisplayConfig, callback, packageName);
         }
 
         @Override // Binder call
@@ -625,9 +623,6 @@
 
     private final class LocalService extends VirtualDeviceManagerInternal {
         @GuardedBy("mVirtualDeviceManagerLock")
-        private final ArrayList<VirtualDisplayListener>
-                mVirtualDisplayListeners = new ArrayList<>();
-        @GuardedBy("mVirtualDeviceManagerLock")
         private final ArrayList<AppsOnVirtualDeviceListener>
                 mAppsOnVirtualDeviceListeners = new ArrayList<>();
         @GuardedBy("mVirtualDeviceManagerLock")
@@ -665,35 +660,15 @@
         }
 
         @Override
-        public void onVirtualDisplayCreated(int displayId) {
-            final VirtualDisplayListener[] listeners;
-            synchronized (mVirtualDeviceManagerLock) {
-                listeners = mVirtualDisplayListeners.toArray(new VirtualDisplayListener[0]);
-            }
-            mHandler.post(() -> {
-                for (VirtualDisplayListener listener : listeners) {
-                    listener.onVirtualDisplayCreated(displayId);
-                }
-            });
-        }
-
-        @Override
         public void onVirtualDisplayRemoved(IVirtualDevice virtualDevice, int displayId) {
-            final VirtualDisplayListener[] listeners;
             VirtualDeviceImpl virtualDeviceImpl;
             synchronized (mVirtualDeviceManagerLock) {
-                listeners = mVirtualDisplayListeners.toArray(new VirtualDisplayListener[0]);
                 virtualDeviceImpl = mVirtualDevices.get(
                         ((VirtualDeviceImpl) virtualDevice).getDeviceId());
             }
             if (virtualDeviceImpl != null) {
                 virtualDeviceImpl.onVirtualDisplayRemoved(displayId);
             }
-            mHandler.post(() -> {
-                for (VirtualDisplayListener listener : listeners) {
-                    listener.onVirtualDisplayRemoved(displayId);
-                }
-            });
         }
 
         @Override
@@ -799,22 +774,6 @@
         }
 
         @Override
-        public void registerVirtualDisplayListener(
-                @NonNull VirtualDisplayListener listener) {
-            synchronized (mVirtualDeviceManagerLock) {
-                mVirtualDisplayListeners.add(listener);
-            }
-        }
-
-        @Override
-        public void unregisterVirtualDisplayListener(
-                @NonNull VirtualDisplayListener listener) {
-            synchronized (mVirtualDeviceManagerLock) {
-                mVirtualDisplayListeners.remove(listener);
-            }
-        }
-
-        @Override
         public void registerAppsOnVirtualDeviceListener(
                 @NonNull AppsOnVirtualDeviceListener listener) {
             synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 15fc2dc..f6835fe 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3651,7 +3651,26 @@
             Watchdog.getInstance().pauseWatchingMonitorsFor(
                 SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, "#close might be slow");
             if (mMounted) {
-                mVold.unmountAppFuse(uid, mountId);
+                BackgroundThread.getHandler().post(() -> {
+                    try {
+                        // We need to run the unmount on a separate thread to
+                        // prevent a possible deadlock, where:
+                        // 1. AppFuseThread (this thread) tries to call into vold
+                        // 2. the vold lock is held by another thread, which called:
+                        //    mVold.openAppFuseFile()
+                        //    as part of that call, vold calls open() on the
+                        //    underlying file, which is a call that needs to be
+                        //    handled by the AppFuseThread, which is stuck waiting
+                        //    for the vold lock (see 1.)
+                        // It is safe to do the unmount asynchronously, because the mount
+                        // path we use is never reused during the current boot cycle;
+                        // see mNextAppFuseName. Also,we have anyway stopped serving
+                        // requests at this point.
+                        mVold.unmountAppFuse(uid, mountId);
+                    } catch (RemoteException e) {
+                        throw e.rethrowAsRuntimeException();
+                    }
+                });
                 mMounted = false;
             }
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b99a98f..f92af67 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -599,6 +599,9 @@
     private static final String INTENT_REMOTE_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.REMOTE_BUGREPORT_FINISHED";
 
+    public static final String DATA_FILE_PATH_HEADER = "Data File: ";
+    public static final String DATA_FILE_PATH_FOOTER = "End Data File\n";
+
     // If set, we will push process association information in to procstats.
     static final boolean TRACK_PROCSTATS_ASSOCIATIONS = true;
 
@@ -9595,17 +9598,33 @@
                         : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0);
                 int dropboxMaxSize = Settings.Global.getInt(
                         mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE);
-                int maxDataFileSize = dropboxMaxSize - sb.length()
-                        - lines * RESERVED_BYTES_PER_LOGCAT_LINE;
 
-                if (dataFile != null && maxDataFileSize > 0) {
-                    try {
-                        sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
-                                    "\n\n[[TRUNCATED]]"));
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error reading " + dataFile, e);
+                if (dataFile != null) {
+                    // Attach the stack traces file to the report so collectors can load them
+                    // by file if they have access.
+                    sb.append(DATA_FILE_PATH_HEADER)
+                            .append(dataFile.getAbsolutePath()).append('\n');
+
+                    int maxDataFileSize = dropboxMaxSize
+                            - sb.length()
+                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
+                            - DATA_FILE_PATH_FOOTER.length();
+
+                    if (maxDataFileSize > 0) {
+                        // Inline dataFile contents if there is room.
+                        try {
+                            sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
+                                    "\n\n[[TRUNCATED]]\n"));
+                        } catch (IOException e) {
+                            Slog.e(TAG, "Error reading " + dataFile, e);
+                        }
                     }
+
+                    // Always append the footer, even there wasn't enough space to inline the
+                    // dataFile contents.
+                    sb.append(DATA_FILE_PATH_FOOTER);
                 }
+
                 if (crashInfo != null && crashInfo.stackTrace != null) {
                     sb.append(crashInfo.stackTrace);
                 }
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
index 51cb950..5c8dd0d 100644
--- a/services/core/java/com/android/server/audio/AdiDeviceState.java
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -25,6 +25,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.Utils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -167,8 +168,9 @@
     public String toString() {
         return "type: " + mDeviceType
                 + " internal type: 0x" + Integer.toHexString(mInternalDeviceType)
-                + " addr: " + mDeviceAddress + " bt audio type: "
-                + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
+                + " addr: " + Utils.anonymizeBluetoothAddress(mInternalDeviceType, mDeviceAddress)
+                + " bt audio type: "
+                        + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
                 + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker
                 + " HTenabled: " + mHeadTrackerEnabled;
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 7ba0827..e9b102b 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -48,6 +48,7 @@
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
 import android.media.MediaRecorder.AudioSource;
+import android.media.Utils;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
@@ -477,7 +478,7 @@
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
                     + " (" + AudioSystem.getDeviceName(mDeviceType)
                     + ") name:" + mDeviceName
-                    + " addr:" + mDeviceAddress
+                    + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress)
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
                     + " peer addr:" + mPeerDeviceAddress
                     + " group:" + mGroupId
@@ -532,7 +533,7 @@
         mApmConnectedDevices.forEach((keyType, valueAddress) -> {
             pw.println("  " + prefix + " type:0x" + Integer.toHexString(keyType)
                     + " (" + AudioSystem.getDeviceName(keyType)
-                    + ") addr:" + valueAddress); });
+                    + ") addr:" + Utils.anonymizeBluetoothAddress(keyType, valueAddress)); });
         pw.println("\n" + prefix + "Preferred devices for capture preset:");
         mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
             pw.println("  " + prefix + "capturePreset:" + capturePreset
@@ -1789,7 +1790,8 @@
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " now available").printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " now available").printLog(TAG));
         }
 
         // Reset A2DP suspend state each time a new sink is connected
@@ -2027,7 +2029,8 @@
                 .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
             // removing A2DP device not currently used by AudioPolicy, log but don't act on it
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
+                    "A2DP device " + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable, was not used")).printLog(TAG));
             mmi.set(MediaMetrics.Property.EARLY_RETURN,
                     "A2DP device made unavailable, was not used")
                     .record();
@@ -2043,13 +2046,15 @@
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "APM failed to make unavailable A2DP device addr=" + address
+                    "APM failed to make unavailable A2DP device addr="
+                            + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printLog(TAG));
             // TODO:  failed to disconnect, stop here
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable")).printLog(TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
 
@@ -2238,7 +2243,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " now available").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " now available").printLog(TAG));
             }
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
@@ -2282,7 +2288,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " made unavailable").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " made unavailable").printLog(TAG));
             }
             mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b2ee610..1e38c0f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -136,6 +136,7 @@
 import android.media.MediaRecorder.AudioSource;
 import android.media.PlayerBase;
 import android.media.Spatializer;
+import android.media.Utils;
 import android.media.VolumeInfo;
 import android.media.VolumePolicy;
 import android.media.audiofx.AudioEffect;
@@ -7470,7 +7471,7 @@
 
         sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                 + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
-                + device.getAddress() + " behavior:"
+                + Utils.anonymizeBluetoothAddress(device.getAddress()) + " behavior:"
                 + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
                 + " pack:" + pkgName).printLog(TAG));
         if (pkgName == null) {
@@ -9641,7 +9642,7 @@
     private void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
         sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
-                + address + " support=" + support).printLog(TAG));
+                + Utils.anonymizeBluetoothAddress(address) + " support=" + support).printLog(TAG));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         setAvrcpAbsoluteVolumeSupported(support);
     }
@@ -10539,11 +10540,11 @@
     AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) {
         Objects.requireNonNull(ada);
         if (AudioSystem.isBluetoothDevice(ada.getInternalType())) {
-            String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress());
+            String anonymizedAddress = Utils.anonymizeBluetoothAddress(ada.getAddress());
             for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
                 if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
                         && (ada.getInternalType() == ads.getInternalDeviceType())
-                        && anonymizedAddress.equals(anonymizeBluetoothAddress(
+                        && anonymizedAddress.equals(Utils.anonymizeBluetoothAddress(
                                 ads.getDeviceAddress())))) {
                     continue;
                 }
@@ -10554,19 +10555,6 @@
         return ada;
     }
 
-    /**
-     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
-     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
-     * @param address Mac address to be anonymized
-     * @return anonymized mac address
-     */
-    static String anonymizeBluetoothAddress(String address) {
-        if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) {
-            return null;
-        }
-        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
-    }
-
     private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList(
                 List<AudioDeviceAttributes> devices) {
         if (isBluetoothPrividged()) {
@@ -10590,7 +10578,7 @@
             return ada;
         }
         AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
-        res.setAddress(anonymizeBluetoothAddress(ada.getAddress()));
+        res.setAddress(Utils.anonymizeBluetoothAddress(ada.getAddress()));
         return res;
     }
 
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 283353dd..0d7f778 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -32,29 +32,12 @@
  */
 public abstract class VirtualDeviceManagerInternal {
 
-    /** Interface to listen to the creation and destruction of virtual displays. */
-    public interface VirtualDisplayListener {
-        /** Notifies that a virtual display was created. */
-        void onVirtualDisplayCreated(int displayId);
-
-        /** Notifies that a virtual display was removed. */
-        void onVirtualDisplayRemoved(int displayId);
-    }
-
     /** Interface to listen to the changes on the list of app UIDs running on any virtual device. */
     public interface AppsOnVirtualDeviceListener {
         /** Notifies that running apps on any virtual device has changed */
         void onAppsOnAnyVirtualDeviceChanged(Set<Integer> allRunningUids);
     }
 
-    /** Register a listener for the creation and destruction of virtual displays. */
-    public abstract void registerVirtualDisplayListener(
-            @NonNull VirtualDisplayListener listener);
-
-    /** Unregister a listener for the creation and destruction of virtual displays. */
-    public abstract void unregisterVirtualDisplayListener(
-            @NonNull VirtualDisplayListener listener);
-
     /** Register a listener for changes of running app UIDs on any virtual device. */
     public abstract void registerAppsOnVirtualDeviceListener(
             @NonNull AppsOnVirtualDeviceListener listener);
@@ -104,13 +87,6 @@
     public abstract @NonNull ArraySet<Integer> getDeviceIdsForUid(int uid);
 
     /**
-     * Notifies that a virtual display is created.
-     *
-     * @param displayId The display id of the created virtual display.
-     */
-    public abstract void onVirtualDisplayCreated(int displayId);
-
-    /**
      * Notifies that a virtual display is removed.
      *
      * @param virtualDevice The virtual device where the virtual display located.
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 53fbe8f..a12243b 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -22,7 +22,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.RouteInfo.RTN_THROW;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN;
 import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
@@ -45,12 +44,10 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -113,7 +110,6 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.FileUtils;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.INetworkManagementService;
@@ -152,7 +148,6 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.NetdUtils;
@@ -202,7 +197,6 @@
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * @hide
@@ -1063,8 +1057,6 @@
         // Store mPackage since it might be reset or might be replaced with the other VPN app.
         final String oldPackage = mPackage;
         final boolean isPackageChanged = !Objects.equals(packageName, oldPackage);
-        // TODO: Remove "SdkLevel.isAtLeastT()" check once VpnManagerService is decoupled from
-        //  ConnectivityServiceTest.
         // Only notify VPN apps that were already always-on, and only if the always-on provider
         // changed, or the lockdown mode changed.
         final boolean shouldNotifyOldPkg = isVpnApp(oldPackage) && mAlwaysOn
@@ -1078,12 +1070,6 @@
 
         saveAlwaysOnPackage();
 
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (!SdkLevel.isAtLeastT()) {
-            return true;
-        }
-
         if (shouldNotifyOldPkg) {
             // If both of shouldNotifyOldPkg & isPackageChanged are true, that means the
             // always-on of old package is disabled or the old package is replaced with the new
@@ -1984,9 +1970,7 @@
         for (String app : packageNames) {
             int uid = getAppUid(mContext, app, userId);
             if (uid != -1) uids.add(uid);
-            // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-            // ConnectivityServiceTest.
-            if (Process.isApplicationUid(uid) && SdkLevel.isAtLeastT()) {
+            if (Process.isApplicationUid(uid)) {
                 uids.add(Process.toSdkSandboxUid(uid));
             }
         }
@@ -2297,15 +2281,6 @@
 
     private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() {
         @Override
-        public void interfaceStatusChanged(String interfaze, boolean up) {
-            synchronized (Vpn.this) {
-                if (!up && mVpnRunner != null && mVpnRunner instanceof LegacyVpnRunner) {
-                    ((LegacyVpnRunner) mVpnRunner).exitIfOuterInterfaceIs(interfaze);
-                }
-            }
-        }
-
-        @Override
         public void interfaceRemoved(String interfaze) {
             synchronized (Vpn.this) {
                 if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
@@ -2556,17 +2531,6 @@
     private native boolean jniAddAddress(String interfaze, String address, int prefixLen);
     private native boolean jniDelAddress(String interfaze, String address, int prefixLen);
 
-    private static RouteInfo findIPv4DefaultRoute(LinkProperties prop) {
-        for (RouteInfo route : prop.getAllRoutes()) {
-            // Currently legacy VPN only works on IPv4.
-            if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) {
-                return route;
-            }
-        }
-
-        throw new IllegalStateException("Unable to find IPv4 default gateway");
-    }
-
     private void enforceNotRestrictedUser() {
         final long token = Binder.clearCallingIdentity();
         try {
@@ -2665,10 +2629,6 @@
             throw new SecurityException("Restricted users cannot establish VPNs");
         }
 
-        final RouteInfo ipv4DefaultRoute = findIPv4DefaultRoute(egress);
-        final String gateway = ipv4DefaultRoute.getGateway().getHostAddress();
-        final String iface = ipv4DefaultRoute.getInterface();
-
         // Load certificates.
         String privateKey = "";
         String userCert = "";
@@ -2700,8 +2660,6 @@
             throw new IllegalStateException("Cannot load credentials");
         }
 
-        // Prepare arguments for racoon.
-        String[] racoon = null;
         switch (profile.type) {
             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                 // Secret key is still just the alias (not the actual private key). The private key
@@ -2731,109 +2689,9 @@
                 // profile.
                 startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN);
                 return;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "udppsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "udprsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "xauthpsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "xauthrsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "hybridrsa",
-                    caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
         }
 
-        // Prepare arguments for mtpd. MTU/MRU calculated conservatively. Only IPv4 supported
-        // because LegacyVpn.
-        // 1500 - 60 (Carrier-internal IPv6 + UDP + GTP) - 10 (PPP) - 16 (L2TP) - 8 (UDP)
-        //   - 77 (IPsec w/ SHA-2 512, 256b trunc-len, AES-CBC) - 8 (UDP encap) - 20 (IPv4)
-        //   - 28 (464xlat)
-        String[] mtpd = null;
-        switch (profile.type) {
-            case VpnProfile.TYPE_PPTP:
-                mtpd = new String[] {
-                    iface, "pptp", profile.server, "1723",
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                    (profile.mppe ? "+mppe" : "nomppe"),
-                };
-                if (profile.mppe) {
-                    // Disallow PAP authentication when MPPE is requested, as MPPE cannot work
-                    // with PAP anyway, and users may not expect PAP (plain text) to be used when
-                    // MPPE was requested.
-                    mtpd = Arrays.copyOf(mtpd, mtpd.length + 1);
-                    mtpd[mtpd.length - 1] = "-pap";
-                }
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                mtpd = new String[] {
-                    iface, "l2tp", profile.server, "1701", profile.l2tpSecret,
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                };
-                break;
-        }
-
-        VpnConfig config = new VpnConfig();
-        config.legacy = true;
-        config.user = profile.key;
-        config.interfaze = iface;
-        config.session = profile.name;
-        config.isMetered = false;
-        config.proxyInfo = profile.proxy;
-        if (underlying != null) {
-            config.underlyingNetworks = new Network[] { underlying };
-        }
-
-        config.addLegacyRoutes(profile.routes);
-        if (!profile.dnsServers.isEmpty()) {
-            config.dnsServers = Arrays.asList(profile.dnsServers.split(" +"));
-        }
-        if (!profile.searchDomains.isEmpty()) {
-            config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
-        }
-        startLegacyVpn(config, racoon, mtpd, profile);
-    }
-
-    private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd,
-            VpnProfile profile) {
-        stopVpnRunnerPrivileged();
-
-        // Prepare for the new request.
-        prepareInternal(VpnConfig.LEGACY_VPN);
-        updateState(DetailedState.CONNECTING, "startLegacyVpn");
-
-        // Start a new LegacyVpnRunner and we are done!
-        mVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile);
-        startLegacyVpnRunner();
-    }
-
-    @VisibleForTesting
-    protected void startLegacyVpnRunner() {
-        mVpnRunner.start();
+        throw new UnsupportedOperationException("Legacy VPN is deprecated");
     }
 
     /**
@@ -2851,17 +2709,7 @@
             return;
         }
 
-        final boolean isLegacyVpn = mVpnRunner instanceof LegacyVpnRunner;
         mVpnRunner.exit();
-
-        // LegacyVpn uses daemons that must be shut down before new ones are brought up.
-        // The same limitation does not apply to Platform VPNs.
-        if (isLegacyVpn) {
-            synchronized (LegacyVpnRunner.TAG) {
-                // wait for old thread to completely finish before spinning up
-                // new instance, otherwise state updates can be out of order.
-            }
-        }
     }
 
     /**
@@ -4143,9 +3991,7 @@
                 // Ignore stale runner.
                 if (mVpnRunner != this) return;
 
-                // TODO(b/230548427): Remove SDK check once VPN related stuff are
-                //  decoupled from ConnectivityServiceTest.
-                if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) {
+                if (category != null && isVpnApp(mPackage)) {
                     sendEventToVpnManagerApp(category, errorClass, errorCode,
                             getPackage(), mSessionKey, makeVpnProfileStateLocked(),
                             mActiveNetwork,
@@ -4256,343 +4102,6 @@
         }
     }
 
-    /**
-     * Bringing up a VPN connection takes time, and that is all this thread
-     * does. Here we have plenty of time. The only thing we need to take
-     * care of is responding to interruptions as soon as possible. Otherwise
-     * requests will pile up. This could be done in a Handler as a state
-     * machine, but it is much easier to read in the current form.
-     */
-    private class LegacyVpnRunner extends VpnRunner {
-        private static final String TAG = "LegacyVpnRunner";
-
-        private final String[] mDaemons;
-        private final String[][] mArguments;
-        private final LocalSocket[] mSockets;
-        private final String mOuterInterface;
-        private final AtomicInteger mOuterConnection =
-                new AtomicInteger(ConnectivityManager.TYPE_NONE);
-        private final VpnProfile mProfile;
-
-        private long mBringupStartTime = -1;
-
-        /**
-         * Watch for the outer connection (passing in the constructor) going away.
-         */
-        private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (!mEnableTeardown) return;
-
-                if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-                    if (intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
-                            ConnectivityManager.TYPE_NONE) == mOuterConnection.get()) {
-                        NetworkInfo info = (NetworkInfo)intent.getExtra(
-                                ConnectivityManager.EXTRA_NETWORK_INFO);
-                        if (info != null && !info.isConnectedOrConnecting()) {
-                            try {
-                                mObserver.interfaceStatusChanged(mOuterInterface, false);
-                            } catch (RemoteException e) {}
-                        }
-                    }
-                }
-            }
-        };
-
-        // GuardedBy("Vpn.this") (annotation can't be applied to constructor)
-        LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {
-            super(TAG);
-            if (racoon == null && mtpd == null) {
-                throw new IllegalArgumentException(
-                        "Arguments to racoon and mtpd must not both be null");
-            }
-            mConfig = config;
-            mDaemons = new String[] {"racoon", "mtpd"};
-            // TODO: clear arguments from memory once launched
-            mArguments = new String[][] {racoon, mtpd};
-            mSockets = new LocalSocket[mDaemons.length];
-
-            // This is the interface which VPN is running on,
-            // mConfig.interfaze will change to point to OUR
-            // internal interface soon. TODO - add inner/outer to mconfig
-            // TODO - we have a race - if the outer iface goes away/disconnects before we hit this
-            // we will leave the VPN up.  We should check that it's still there/connected after
-            // registering
-            mOuterInterface = mConfig.interfaze;
-
-            mProfile = profile;
-
-            if (!TextUtils.isEmpty(mOuterInterface)) {
-                for (Network network : mConnectivityManager.getAllNetworks()) {
-                    final LinkProperties lp = mConnectivityManager.getLinkProperties(network);
-                    if (lp != null && lp.getAllInterfaceNames().contains(mOuterInterface)) {
-                        final NetworkInfo netInfo = mConnectivityManager.getNetworkInfo(network);
-                        if (netInfo != null) {
-                            mOuterConnection.set(netInfo.getType());
-                            break;
-                        }
-                    }
-                }
-            }
-
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            mContext.registerReceiver(mBroadcastReceiver, filter);
-        }
-
-        /**
-         * Checks if the parameter matches the underlying interface
-         *
-         * <p>If the underlying interface is torn down, the LegacyVpnRunner also should be. It has
-         * no ability to migrate between interfaces (or Networks).
-         */
-        public void exitIfOuterInterfaceIs(String interfaze) {
-            if (interfaze.equals(mOuterInterface)) {
-                Log.i(TAG, "Legacy VPN is going down with " + interfaze);
-                exitVpnRunner();
-            }
-        }
-
-        /** Tears down this LegacyVpn connection */
-        @Override
-        public void exitVpnRunner() {
-            // We assume that everything is reset after stopping the daemons.
-            interrupt();
-
-            // Always disconnect. This may be called again in cleanupVpnStateLocked() if
-            // exitVpnRunner() was called from exit(), but it will be a no-op.
-            agentDisconnect();
-            try {
-                mContext.unregisterReceiver(mBroadcastReceiver);
-            } catch (IllegalArgumentException e) {}
-        }
-
-        @Override
-        public void run() {
-            // Wait for the previous thread since it has been interrupted.
-            Log.v(TAG, "Waiting");
-            synchronized (TAG) {
-                Log.v(TAG, "Executing");
-                try {
-                    bringup();
-                    waitForDaemonsToStop();
-                    interrupted(); // Clear interrupt flag if execute called exit.
-                } catch (InterruptedException e) {
-                } finally {
-                    for (LocalSocket socket : mSockets) {
-                        IoUtils.closeQuietly(socket);
-                    }
-                    // This sleep is necessary for racoon to successfully complete sending delete
-                    // message to server.
-                    try {
-                        Thread.sleep(50);
-                    } catch (InterruptedException e) {
-                    }
-                    for (String daemon : mDaemons) {
-                        mDeps.stopService(daemon);
-                    }
-                }
-                agentDisconnect();
-            }
-        }
-
-        private void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException {
-            long now = SystemClock.elapsedRealtime();
-            if (now - mBringupStartTime <= 60000) {
-                Thread.sleep(sleepLonger ? 200 : 1);
-            } else {
-                updateState(DetailedState.FAILED, "checkpoint");
-                throw new IllegalStateException("VPN bringup took too long");
-            }
-        }
-
-        private void checkAndFixupArguments(@NonNull final InetAddress endpointAddress) {
-            final String endpointAddressString = endpointAddress.getHostAddress();
-            // Perform some safety checks before inserting the address in place.
-            // Position 0 in mDaemons and mArguments must be racoon, and position 1 must be mtpd.
-            if (!"racoon".equals(mDaemons[0]) || !"mtpd".equals(mDaemons[1])) {
-                throw new IllegalStateException("Unexpected daemons order");
-            }
-
-            // Respectively, the positions at which racoon and mtpd take the server address
-            // argument are 1 and 2. Not all types of VPN require both daemons however, and
-            // in that case the corresponding argument array is null.
-            if (mArguments[0] != null) {
-                if (!mProfile.server.equals(mArguments[0][1])) {
-                    throw new IllegalStateException("Invalid server argument for racoon");
-                }
-                mArguments[0][1] = endpointAddressString;
-            }
-
-            if (mArguments[1] != null) {
-                if (!mProfile.server.equals(mArguments[1][2])) {
-                    throw new IllegalStateException("Invalid server argument for mtpd");
-                }
-                mArguments[1][2] = endpointAddressString;
-            }
-        }
-
-        private void bringup() {
-            // Catch all exceptions so we can clean up a few things.
-            try {
-                // resolve never returns null. If it does because of some bug, it will be
-                // caught by the catch() block below and cleanup gracefully.
-                final InetAddress endpointAddress = mDeps.resolve(mProfile.server);
-
-                // Big hack : dynamically replace the address of the server in the arguments
-                // with the resolved address.
-                checkAndFixupArguments(endpointAddress);
-
-                // Initialize the timer.
-                mBringupStartTime = SystemClock.elapsedRealtime();
-
-                // Wait for the daemons to stop.
-                for (String daemon : mDaemons) {
-                    while (!mDeps.isServiceStopped(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-                }
-
-                // Clear the previous state.
-                final File state = mDeps.getStateFile();
-                state.delete();
-                if (state.exists()) {
-                    throw new IllegalStateException("Cannot delete the state");
-                }
-                new File("/data/misc/vpn/abort").delete();
-
-                updateState(DetailedState.CONNECTING, "execute");
-
-                // Start the daemon with arguments.
-                for (int i = 0; i < mDaemons.length; ++i) {
-                    String[] arguments = mArguments[i];
-                    if (arguments == null) {
-                        continue;
-                    }
-
-                    // Start the daemon.
-                    String daemon = mDaemons[i];
-                    mDeps.startService(daemon);
-
-                    // Wait for the daemon to start.
-                    while (!mDeps.isServiceRunning(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-
-                    // Create the control socket.
-                    mSockets[i] = new LocalSocket();
-
-                    // Wait for the socket to connect and send over the arguments.
-                    mDeps.sendArgumentsToDaemon(daemon, mSockets[i], arguments,
-                            this::checkInterruptAndDelay);
-                }
-
-                // Wait for the daemons to create the new state.
-                while (!state.exists()) {
-                    // Check if a running daemon is dead.
-                    for (int i = 0; i < mDaemons.length; ++i) {
-                        String daemon = mDaemons[i];
-                        if (mArguments[i] != null && !mDeps.isServiceRunning(daemon)) {
-                            throw new IllegalStateException(daemon + " is dead");
-                        }
-                    }
-                    checkInterruptAndDelay(true);
-                }
-
-                // Now we are connected. Read and parse the new state.
-                String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1);
-                if (parameters.length != 7) {
-                    throw new IllegalStateException("Cannot parse the state: '"
-                            + String.join("', '", parameters) + "'");
-                }
-
-                // Set the interface and the addresses in the config.
-                synchronized (Vpn.this) {
-                    mConfig.interfaze = parameters[0].trim();
-
-                    mConfig.addLegacyAddresses(parameters[1]);
-                    // Set the routes if they are not set in the config.
-                    if (mConfig.routes == null || mConfig.routes.isEmpty()) {
-                        mConfig.addLegacyRoutes(parameters[2]);
-                    }
-
-                    // Set the DNS servers if they are not set in the config.
-                    if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) {
-                        String dnsServers = parameters[3].trim();
-                        if (!dnsServers.isEmpty()) {
-                            mConfig.dnsServers = Arrays.asList(dnsServers.split(" "));
-                        }
-                    }
-
-                    // Set the search domains if they are not set in the config.
-                    if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) {
-                        String searchDomains = parameters[4].trim();
-                        if (!searchDomains.isEmpty()) {
-                            mConfig.searchDomains = Arrays.asList(searchDomains.split(" "));
-                        }
-                    }
-
-                    // Add a throw route for the VPN server endpoint, if one was specified.
-                    if (endpointAddress instanceof Inet4Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 32), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else if (endpointAddress instanceof Inet6Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 128), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else {
-                        Log.e(TAG, "Unknown IP address family for VPN endpoint: "
-                                + endpointAddress);
-                    }
-
-                    // Here is the last step and it must be done synchronously.
-                    // Set the start time
-                    mConfig.startTime = SystemClock.elapsedRealtime();
-
-                    // Check if the thread was interrupted while we were waiting on the lock.
-                    checkInterruptAndDelay(false);
-
-                    // Check if the interface is gone while we are waiting.
-                    if (!mDeps.isInterfacePresent(Vpn.this, mConfig.interfaze)) {
-                        throw new IllegalStateException(mConfig.interfaze + " is gone");
-                    }
-
-                    // Now INetworkManagementEventObserver is watching our back.
-                    mInterface = mConfig.interfaze;
-                    prepareStatusIntent();
-
-                    agentConnect();
-
-                    Log.i(TAG, "Connected!");
-                }
-            } catch (Exception e) {
-                Log.i(TAG, "Aborting", e);
-                updateState(DetailedState.FAILED, e.getMessage());
-                exitVpnRunner();
-            }
-        }
-
-        /**
-         * Check all daemons every two seconds. Return when one of them is stopped.
-         * The caller will move to the disconnected state when this function returns,
-         * which can happen if a daemon failed or if the VPN was torn down.
-         */
-        private void waitForDaemonsToStop() throws InterruptedException {
-            if (!mNetworkInfo.isConnected()) {
-                return;
-            }
-            while (true) {
-                Thread.sleep(2000);
-                for (int i = 0; i < mDaemons.length; i++) {
-                    if (mArguments[i] != null && mDeps.isServiceStopped(mDaemons[i])) {
-                        return;
-                    }
-                }
-            }
-        }
-    }
-
     private void verifyCallingUidAndPackage(String packageName) {
         mDeps.verifyCallingUidAndPackage(mContext, packageName, mUserId);
     }
@@ -4839,11 +4348,9 @@
         // Build intent first because the sessionKey will be reset after performing
         // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
         // VpnRunner.exit() to prevent design being changed in the future.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
         final int ownerUid = mOwnerUID;
         Intent intent = null;
-        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
+        if (isVpnApp(mPackage)) {
             intent = buildVpnManagerEventIntent(
                     VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
                     -1 /* errorClass */, -1 /* errorCode*/, mPackage,
@@ -4884,12 +4391,8 @@
         // The underlying network, NetworkCapabilities and LinkProperties are not
         // necessary to send to VPN app since the purpose of this event is to notify
         // VPN app that VPN is deactivated by the user.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (SdkLevel.isAtLeastT()) {
-            mEventChanges.log("[VMEvent] " + packageName + " stopped");
-            sendEventToVpnManagerApp(intent, packageName);
-        }
+        mEventChanges.log("[VMEvent] " + packageName + " stopped");
+        sendEventToVpnManagerApp(intent, packageName);
     }
 
     private boolean storeAppExclusionList(@NonNull String packageName,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1bdd402..2ec3700 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -179,6 +179,8 @@
 import android.app.role.RoleManager;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -546,6 +548,15 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION = 264179692L;
 
+    /**
+     * App calls to {@link android.app.NotificationManager#setInterruptionFilter} and
+     * {@link android.app.NotificationManager#setNotificationPolicy} manage DND through the
+     * creation and activation of an implicit {@link android.app.AutomaticZenRule}.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
+
     private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30);
 
     private IActivityManager mAm;
@@ -5343,6 +5354,12 @@
             if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
             final int callingUid = Binder.getCallingUid();
             final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen);
+                return;
+            }
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter",
@@ -5426,6 +5443,16 @@
             }
         }
 
+        private boolean canManageGlobalZenPolicy(String callingPkg, int callingUid) {
+            boolean isCompatChangeEnabled = Binder.withCleanCallingIdentity(
+                    () -> CompatChanges.isChangeEnabled(MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES,
+                            callingUid));
+            return !isCompatChangeEnabled
+                    || isCallerIsSystemOrSystemUi()
+                    || hasCompanionDevice(callingPkg, UserHandle.getUserId(callingUid),
+                            AssociationRequest.DEVICE_PROFILE_WATCH);
+        }
+
         private void enforcePolicyAccess(String pkg, String method) {
             if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission(
                     android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
@@ -5619,6 +5646,10 @@
 
         @Override
         public Policy getNotificationPolicy(String pkg) {
+            final int callingUid = Binder.getCallingUid();
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(pkg);
+            }
             final long identity = Binder.clearCallingIdentity();
             try {
                 return mZenModeHelper.getNotificationPolicy();
@@ -5649,6 +5680,10 @@
             enforcePolicyAccess(pkg, "setNotificationPolicy");
             int callingUid = Binder.getCallingUid();
             boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
+                    && !canManageGlobalZenPolicy(pkg, callingUid);
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg,
@@ -5687,16 +5722,21 @@
                 policy = new Policy(policy.priorityCategories,
                         policy.priorityCallSenders, policy.priorityMessageSenders,
                         newVisualEffects, policy.priorityConversationSenders);
-                ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy);
-                mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+
+                if (shouldApplyAsImplicitRule) {
+                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
+                } else {
+                    ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
+                            policy);
+                    mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+                }
             } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set notification policy", e);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
 
-
-
         @Override
         public List<String> getEnabledNotificationListenerPackages() {
             checkCallerIsSystem();
@@ -10556,6 +10596,12 @@
     }
 
     boolean hasCompanionDevice(ManagedServiceInfo info) {
+        return hasCompanionDevice(info.component.getPackageName(),
+                info.userid, /* withDeviceProfile= */ null);
+    }
+
+    private boolean hasCompanionDevice(String pkg, @UserIdInt int userId,
+            @Nullable @AssociationRequest.DeviceProfile String withDeviceProfile) {
         if (mCompanionManager == null) {
             mCompanionManager = getCompanionManager();
         }
@@ -10565,17 +10611,19 @@
         }
         final long identity = Binder.clearCallingIdentity();
         try {
-            List<?> associations = mCompanionManager.getAssociations(
-                    info.component.getPackageName(), info.userid);
-            if (!ArrayUtils.isEmpty(associations)) {
-                return true;
+            List<AssociationInfo> associations = mCompanionManager.getAssociations(pkg, userId);
+            for (AssociationInfo association : associations) {
+                if (withDeviceProfile == null || withDeviceProfile.equals(
+                        association.getDeviceProfile())) {
+                    return true;
+                }
             }
         } catch (SecurityException se) {
             // Not a privileged listener
         } catch (RemoteException re) {
             Slog.e(TAG, "Cannot reach companion device service", re);
         } catch (Exception e) {
-            Slog.e(TAG, "Cannot verify listener " + info, e);
+            Slog.e(TAG, "Cannot verify caller pkg=" + pkg + ", userId=" + userId, e);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -11401,17 +11449,16 @@
                             }
                         }
                     }
-                }
 
-                // clean up anything in the disallowed pkgs list
-                for (int i = 0; i < pkgList.length; i++) {
-                    String pkg = pkgList[i];
-                    for (int j = mRequestedNotificationListeners.size() - 1; j >= 0; j--) {
-                        NotificationListenerFilter nlf =
-                                mRequestedNotificationListeners.valueAt(j);
-
-                        VersionedPackage ai = new VersionedPackage(pkg, uidList[i]);
-                        nlf.removePackage(ai);
+                    // Clean up removed package from the disallowed packages list
+                    for (int i = 0; i < pkgList.length; i++) {
+                        String pkg = pkgList[i];
+                        for (int j = mRequestedNotificationListeners.size() - 1; j >= 0; j--) {
+                            NotificationListenerFilter nlf =
+                                    mRequestedNotificationListeners.valueAt(j);
+                            VersionedPackage ai = new VersionedPackage(pkg, uidList[i]);
+                            nlf.removePackage(ai);
+                        }
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 8f5676b3..9106c33 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -118,7 +118,10 @@
 
     protected boolean canSnooze(int numberToSnooze) {
         synchronized (mLock) {
-            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
+            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
+                    || (mPersistedSnoozedNotifications.size()
+                    + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze)
+                    > CONCURRENT_SNOOZE_LIMIT) {
                 return false;
             }
         }
@@ -357,6 +360,9 @@
 
             if (groupSummaryKey != null) {
                 NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
+                String trimmedKey = getTrimmedString(groupSummaryKey);
+                mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
+                mPersistedSnoozedNotifications.remove(trimmedKey);
 
                 if (record != null && !record.isCanceled) {
                     Runnable runnable = () -> {
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
new file mode 100644
index 0000000..2a65aff
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenAdapters.java
@@ -0,0 +1,78 @@
+/*
+ * 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.server.notification;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+/**
+ * Converters between different Zen representations.
+ */
+class ZenAdapters {
+
+    static ZenPolicy notificationPolicyToZenPolicy(Policy policy) {
+        ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder()
+                .allowAlarms(policy.allowAlarms())
+                .allowCalls(
+                        policy.allowCalls()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowConversations(
+                        policy.allowConversations()
+                                ? notificationPolicyConversationSendersToZenPolicy(
+                                        policy.allowConversationsFrom())
+                                : ZenPolicy.CONVERSATION_SENDERS_NONE)
+                .allowEvents(policy.allowEvents())
+                .allowMedia(policy.allowMedia())
+                .allowMessages(
+                        policy.allowMessages()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowReminders(policy.allowReminders())
+                .allowRepeatCallers(policy.allowRepeatCallers())
+                .allowSystem(policy.allowSystem());
+
+        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
+            zenPolicyBuilder.showBadges(policy.showBadges())
+                    .showFullScreenIntent(policy.showFullScreenIntents())
+                    .showInAmbientDisplay(policy.showAmbient())
+                    .showInNotificationList(policy.showInNotificationList())
+                    .showLights(policy.showLights())
+                    .showPeeking(policy.showPeeking())
+                    .showStatusBarIcons(policy.showStatusBarIcons());
+        }
+
+        return zenPolicyBuilder.build();
+    }
+
+    @ZenPolicy.ConversationSenders
+    private static int notificationPolicyConversationSendersToZenPolicy(
+            int npPriorityConversationSenders) {
+        switch (npPriorityConversationSenders) {
+            case Policy.CONVERSATION_SENDERS_ANYONE:
+                return ZenPolicy.CONVERSATION_SENDERS_ANYONE;
+            case Policy.CONVERSATION_SENDERS_IMPORTANT:
+                return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+            case Policy.CONVERSATION_SENDERS_NONE:
+                return ZenPolicy.CONVERSATION_SENDERS_NONE;
+            case Policy.CONVERSATION_SENDERS_UNSET:
+            default:
+                return ZenPolicy.CONVERSATION_SENDERS_UNSET;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 762c1a1..c637df2 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -27,6 +27,7 @@
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
@@ -44,6 +45,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -349,11 +351,11 @@
         ZenRule rule;
         synchronized (mConfigLock) {
             if (mConfig == null) return null;
-             rule = mConfig.automaticRules.get(id);
+            rule = mConfig.automaticRules.get(id);
         }
         if (rule == null) return null;
         if (canManageAutomaticZenRule(rule)) {
-             return createAutomaticZenRule(rule);
+            return zenRuleToAutomaticZenRule(rule);
         }
         return null;
     }
@@ -439,6 +441,167 @@
         }
     }
 
+    /**
+     * Create (or activate, or deactivate) an "implicit" {@link ZenRule} when an app that has
+     * Notification Policy Access but is not allowed to manage the global zen state
+     * calls {@link NotificationManager#setInterruptionFilter}.
+     *
+     * <p>When the {@code zenMode} is {@link Global#ZEN_MODE_OFF}, an existing implicit rule will be
+     * deactivated (if there is no implicit rule, the call will be ignored). For other modes, the
+     * rule's interruption filter will match the supplied {@code zenMode}. The policy of the last
+     * call to {@link NotificationManager#setNotificationPolicy} will be used (or, if never called,
+     * the global policy).
+     *
+     * <p>The created rule is owned by the calling package, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}.
+     *
+     * @param zenMode one of the {@code Global#ZEN_MODE_x} values
+     */
+    void applyGlobalZenModeAsImplicitZenRule(String callingPkg, int callingUid, int zenMode) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            if (zenMode == Global.ZEN_MODE_OFF) {
+                // Deactivate implicit rule if it exists and is active; otherwise ignore.
+                ZenRule rule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule != null) {
+                    Condition deactivated = new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            Condition.STATE_FALSE);
+                    setAutomaticZenRuleState(rule.id, deactivated,
+                            callingUid, /* fromSystemOrSystemUi= */ false);
+                }
+            } else {
+                // Either create a new rule with a default ZenPolicy, or update an existing rule's
+                // filter value. In both cases, also activate (and unsnooze) it.
+                ZenModeConfig newConfig = mConfig.copy();
+                ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule == null) {
+                    rule = newImplicitZenRule(callingPkg);
+                    newConfig.automaticRules.put(rule.id, rule);
+                }
+                rule.zenMode = zenMode;
+                rule.snoozing = false;
+                rule.condition = new Condition(rule.conditionId,
+                        mContext.getString(R.string.zen_mode_implicit_activated),
+                        Condition.STATE_TRUE);
+                setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                        "applyGlobalZenModeAsImplicitZenRule",
+                        callingUid, /* fromSystemOrSystemUi= */ false);
+            }
+        }
+    }
+
+    /**
+     * Create (or update) an "implicit" {@link ZenRule} when an app that has Notification Policy
+     * Access but is not allowed to manage the global zen state calls
+     * {@link NotificationManager#setNotificationPolicy}.
+     *
+     * <p>The created rule is owned by the calling package and has the {@link ZenPolicy}
+     * corresponding to the supplied {@code policy}, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}. Its zen mode will be set to
+     * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
+     */
+    void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
+            NotificationManager.Policy policy) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            ZenModeConfig newConfig = mConfig.copy();
+            ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (rule == null) {
+                rule = newImplicitZenRule(callingPkg);
+                rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+                newConfig.automaticRules.put(rule.id, rule);
+            }
+            // TODO: b/308673679 - Keep user customization of this rule!
+            rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+            setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                    "applyGlobalPolicyAsImplicitZenRule",
+                    callingUid, /* fromSystemOrSystemUi= */ false);
+        }
+    }
+
+    /**
+     * Returns the {@link Policy} associated to the "implicit" {@link ZenRule} of a package that has
+     * Notification Policy Access but is not allowed to manage the global zen state.
+     *
+     * <p>If the implicit rule doesn't exist, or it doesn't specify a {@link ZenPolicy} (because the
+     * app never called {@link NotificationManager#setNotificationPolicy}) then the default policy
+     * is returned (i.e. same as {@link #getNotificationPolicy}.
+     *
+     * <p>Any unset values in the {@link ZenPolicy} will be mapped to their current defaults.
+     */
+    @Nullable
+    Policy getNotificationPolicyFromImplicitZenRule(String callingPkg) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!");
+            return getNotificationPolicy();
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return null;
+            }
+            ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (implicitRule != null && implicitRule.zenPolicy != null) {
+                return mConfig.toNotificationPolicy(implicitRule.zenPolicy);
+            } else {
+                return getNotificationPolicy();
+            }
+        }
+    }
+
+    /**
+     * Creates an empty {@link ZenRule} to be used as the implicit rule for {@code pkg}.
+     * Both {@link ZenRule#zenMode} and {@link ZenRule#zenPolicy} are unset.
+     */
+    private ZenRule newImplicitZenRule(String pkg) {
+        ZenRule rule = new ZenRule();
+        rule.id = implicitRuleId(pkg);
+        rule.pkg = pkg;
+        rule.creationTime = System.currentTimeMillis();
+
+        Binder.withCleanCallingIdentity(() -> {
+            try {
+                ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0);
+                rule.name = applicationInfo.loadLabel(mPm).toString();
+            } catch (PackageManager.NameNotFoundException e) {
+                // Should not happen, since it's the app calling us (?)
+                Log.w(TAG, "Package not found for creating implicit zen rule");
+                rule.name = "Unknown";
+            }
+        });
+
+        rule.condition = null;
+        rule.conditionId = new Uri.Builder()
+                .scheme(Condition.SCHEME)
+                .authority("android")
+                .appendPath("implicit")
+                .appendPath(pkg)
+                .build();
+        rule.enabled = true;
+        rule.modified = false;
+        rule.component = null;
+        rule.configurationActivity = null;
+        return rule;
+    }
+
+    private static String implicitRuleId(String forPackage) {
+        return "implicit_" + forPackage;
+    }
+
     public boolean removeAutomaticZenRule(String id, String reason, int callingUid,
             boolean fromSystemOrSystemUi) {
         ZenModeConfig newConfig;
@@ -626,7 +789,7 @@
                         }
                         // update default rule (if locale changed, name of rule will change)
                         currRule.name = defaultRule.name;
-                        updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule),
+                        updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
                                 "locale changed", callingUid, fromSystemOrSystemUi);
                     }
                 }
@@ -669,7 +832,7 @@
         return null;
     }
 
-    private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
+    private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
             boolean isNew) {
         if (rule.enabled != automaticZenRule.isEnabled()) {
             rule.snoozing = false;
@@ -699,7 +862,7 @@
         }
     }
 
-    protected AutomaticZenRule createAutomaticZenRule(ZenRule rule) {
+    private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
         AutomaticZenRule azr;
         if (Flags.modesApi()) {
             azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 25b7ca1..dcac8c9 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -7,8 +7,6 @@
   bug: "290381858"
 }
 
-
-
 flag {
   name: "polite_notifications"
   namespace: "systemui"
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 77290fd..9f0a975 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -603,40 +603,51 @@
     @NonNull
     private String getEnergyConsumerName(EnergyConsumer consumer,
             EnergyConsumer[] energyConsumers) {
-        if (consumer.type != EnergyConsumerType.OTHER) {
-            StringBuilder sb = new StringBuilder();
-            sb.append(energyConsumerTypeToString(consumer.type));
-            boolean hasOrdinal = consumer.ordinal != 0;
-            if (!hasOrdinal) {
-                // See if any other EnergyConsumer of the same type has an ordinal
-                for (EnergyConsumer aConsumer : energyConsumers) {
-                    if (aConsumer.type == consumer.type && aConsumer.ordinal != 0) {
-                        hasOrdinal = true;
-                        break;
-                    }
+        StringBuilder sb = new StringBuilder();
+        switch (consumer.type) {
+            case EnergyConsumerType.BLUETOOTH:
+                sb.append("BLUETOOTH");
+                break;
+            case EnergyConsumerType.CPU_CLUSTER:
+                sb.append("CPU");
+                break;
+            case EnergyConsumerType.DISPLAY:
+                sb.append("DISPLAY");
+                break;
+            case EnergyConsumerType.GNSS:
+                sb.append("GNSS");
+                break;
+            case EnergyConsumerType.MOBILE_RADIO:
+                sb.append("MOBILE_RADIO");
+                break;
+            case EnergyConsumerType.WIFI:
+                sb.append("WIFI");
+                break;
+            case EnergyConsumerType.CAMERA:
+                sb.append("CAMERA");
+                break;
+            default:
+                if (consumer.name != null && !consumer.name.isBlank()) {
+                    sb.append(consumer.name.toUpperCase(Locale.ENGLISH));
+                } else {
+                    sb.append("CONSUMER_").append(consumer.type);
+                }
+                break;
+        }
+        boolean hasOrdinal = consumer.ordinal != 0;
+        if (!hasOrdinal) {
+            // See if any other EnergyConsumer of the same type has an ordinal
+            for (EnergyConsumer aConsumer : energyConsumers) {
+                if (aConsumer.type == consumer.type && aConsumer.ordinal != 0) {
+                    hasOrdinal = true;
+                    break;
                 }
             }
-            if (hasOrdinal) {
-                sb.append('/').append(consumer.ordinal);
-            }
-            return sb.toString();
-        } else {
-            return consumer.name;
         }
-    }
-
-    private static String energyConsumerTypeToString(int type) {
-        switch(type) {
-            case EnergyConsumerType.BLUETOOTH: return "BLUETOOTH";
-            case EnergyConsumerType.CPU_CLUSTER: return "CPU";
-            case EnergyConsumerType.DISPLAY: return "DISPLAY";
-            case EnergyConsumerType.GNSS: return "GNSS";
-            case EnergyConsumerType.MOBILE_RADIO: return "MOBILE_RADIO";
-            case EnergyConsumerType.WIFI: return "WIFI";
-            case EnergyConsumerType.OTHER: return "";
-            default:
-                throw new IllegalStateException("Unrecognized EnergyConsumerType: " + type);
+        if (hasOrdinal) {
+            sb.append('/').append(consumer.ordinal);
         }
+        return sb.toString();
     }
 
     /**
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index fed6e7e..b2e808a 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -117,16 +117,16 @@
     static final class CallerInfo {
         public final VibrationAttributes attrs;
         public final int uid;
-        public final int displayId;
+        public final int deviceId;
         public final String opPkg;
         public final String reason;
 
-        CallerInfo(@NonNull VibrationAttributes attrs, int uid, int displayId,
-                String opPkg, String reason) {
+        CallerInfo(@NonNull VibrationAttributes attrs, int uid, int deviceId, String opPkg,
+                String reason) {
             Objects.requireNonNull(attrs);
             this.attrs = attrs;
             this.uid = uid;
-            this.displayId = displayId;
+            this.deviceId = deviceId;
             this.opPkg = opPkg;
             this.reason = reason;
         }
@@ -138,14 +138,14 @@
             CallerInfo that = (CallerInfo) o;
             return Objects.equals(attrs, that.attrs)
                     && uid == that.uid
-                    && displayId == that.displayId
+                    && deviceId == that.deviceId
                     && Objects.equals(opPkg, that.opPkg)
                     && Objects.equals(reason, that.reason);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(attrs, uid, displayId, opPkg, reason);
+            return Objects.hash(attrs, uid, deviceId, opPkg, reason);
         }
 
         @Override
@@ -153,7 +153,7 @@
             return "CallerInfo{"
                     + " uid=" + uid
                     + ", opPkg=" + opPkg
-                    + ", displayId=" + displayId
+                    + ", deviceId=" + deviceId
                     + ", attrs=" + attrs
                     + ", reason=" + reason
                     + '}';
@@ -267,8 +267,8 @@
                     mStartTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mStartTime)),
                     mEndTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mEndTime)));
             String callerInfoStr = String.format(Locale.ROOT,
-                    " | %s (uid=%d, displayId=%d) | usage: %s (audio=%s) | flags: %s | reason: %s",
-                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.displayId,
+                    " | %s (uid=%d, deviceId=%d) | usage: %s (audio=%s) | flags: %s | reason: %s",
+                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId,
                     mCallerInfo.attrs.usageToString(),
                     AudioAttributes.usageToString(mCallerInfo.attrs.getAudioUsage()),
                     Long.toBinaryString(mCallerInfo.attrs.getFlags()),
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 7f55836..839c207 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -61,7 +61,6 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
-import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -166,7 +165,6 @@
     final MyUidObserver mUidObserver;
     @VisibleForTesting
     final SettingsBroadcastReceiver mSettingChangeReceiver;
-    final VirtualDeviceListener mVirtualDeviceListener;
 
     @GuardedBy("mLock")
     private final List<OnVibratorSettingsChanged> mListeners = new ArrayList<>();
@@ -180,6 +178,8 @@
     @GuardedBy("mLock")
     @Nullable
     private PowerManagerInternal mPowerManagerInternal;
+    @Nullable
+    private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
 
     @GuardedBy("mLock")
     private boolean mVibrateInputDevices;
@@ -207,8 +207,6 @@
         mSettingObserver = new SettingsContentObserver(handler);
         mUidObserver = new MyUidObserver();
         mSettingChangeReceiver = new SettingsBroadcastReceiver();
-        mVirtualDeviceListener = new VirtualDeviceListener();
-
         mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
                 .getSystemUiServiceComponent().getPackageName();
 
@@ -272,13 +270,6 @@
                     }
                 });
 
-        VirtualDeviceManagerInternal vdm = LocalServices.getService(
-                VirtualDeviceManagerInternal.class);
-        if (vdm != null) {
-            vdm.registerVirtualDisplayListener(mVirtualDeviceListener);
-            vdm.registerAppsOnVirtualDeviceListener(mVirtualDeviceListener);
-        }
-
         registerSettingsChangeReceiver(USER_SWITCHED_INTENT_FILTER);
         registerSettingsChangeReceiver(INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER);
 
@@ -414,8 +405,14 @@
                     && !BACKGROUND_PROCESS_USAGE_ALLOWLIST.contains(usage)) {
                 return Vibration.Status.IGNORED_BACKGROUND;
             }
-            if (mVirtualDeviceListener.isAppOrDisplayOnAnyVirtualDevice(callerInfo.uid,
-                    callerInfo.displayId)) {
+
+            if (callerInfo.deviceId != Context.DEVICE_ID_DEFAULT
+                    && callerInfo.deviceId != Context.DEVICE_ID_INVALID) {
+                return Vibration.Status.IGNORED_FROM_VIRTUAL_DEVICE;
+            }
+
+            if (callerInfo.deviceId == Context.DEVICE_ID_INVALID
+                    && isAppRunningOnAnyVirtualDevice(callerInfo.uid)) {
                 return Vibration.Status.IGNORED_FROM_VIRTUAL_DEVICE;
             }
 
@@ -794,6 +791,15 @@
         return out;
     }
 
+    private boolean isAppRunningOnAnyVirtualDevice(int uid) {
+        if (mVirtualDeviceManagerInternal == null) {
+            mVirtualDeviceManagerInternal =
+                    LocalServices.getService(VirtualDeviceManagerInternal.class);
+        }
+        return mVirtualDeviceManagerInternal != null
+                && mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(uid);
+    }
+
     /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
     @VisibleForTesting
     final class SettingsContentObserver extends ContentObserver {
@@ -853,73 +859,4 @@
             }
         }
     }
-
-    /**
-     * Implementation of Virtual Device listeners for the changes of virtual displays and of apps
-     * running on any virtual device.
-     */
-    final class VirtualDeviceListener implements
-            VirtualDeviceManagerInternal.VirtualDisplayListener,
-            VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener {
-        @GuardedBy("mLock")
-        private final Set<Integer> mVirtualDisplays = new HashSet<>();
-        @GuardedBy("mLock")
-        private final Set<Integer> mAppsOnVirtualDevice = new HashSet<>();
-
-
-        @Override
-        public void onVirtualDisplayCreated(int displayId) {
-            synchronized (mLock) {
-                mVirtualDisplays.add(displayId);
-            }
-        }
-
-        @Override
-        public void onVirtualDisplayRemoved(int displayId) {
-            synchronized (mLock) {
-                mVirtualDisplays.remove(displayId);
-            }
-        }
-
-
-        @Override
-        public void onAppsOnAnyVirtualDeviceChanged(Set<Integer> allRunningUids) {
-            synchronized (mLock) {
-                mAppsOnVirtualDevice.clear();
-                mAppsOnVirtualDevice.addAll(allRunningUids);
-            }
-        }
-
-        /**
-         * @param uid:       uid of the calling app.
-         * @param displayId: the id of a Display.
-         * @return Returns true if:
-         * <ul>
-         *   <li> the displayId is valid, and it's owned by a virtual device.</li>
-         *   <li> the displayId is invalid, and the calling app (uid) is running on a virtual
-         *        device.</li>
-         * </ul>
-         */
-        public boolean isAppOrDisplayOnAnyVirtualDevice(int uid, int displayId) {
-            if (displayId == Display.DEFAULT_DISPLAY) {
-                // The default display is the primary physical display on the phone.
-                return false;
-            }
-
-            synchronized (mLock) {
-                if (displayId == Display.INVALID_DISPLAY) {
-                    // There is no Display object associated with the Context of calling
-                    // {@link SystemVibratorManager}, checking the calling UID instead.
-                    return mAppsOnVirtualDevice.contains(uid);
-                } else {
-                    // Other valid display IDs representing valid logical displays will be
-                    // checked
-                    // against the active virtual displays set built with the registered
-                    // {@link VirtualDisplayListener}.
-                    return mVirtualDisplays.contains(displayId);
-                }
-            }
-        }
-
-    }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index ace7777..cf33cc5 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -63,7 +63,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
-import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -385,7 +384,7 @@
                     return false;
                 }
                 AlwaysOnVibration alwaysOnVibration = new AlwaysOnVibration(alwaysOnId,
-                        new Vibration.CallerInfo(attrs, uid, Display.DEFAULT_DISPLAY, opPkg,
+                        new Vibration.CallerInfo(attrs, uid, Context.DEVICE_ID_DEFAULT, opPkg,
                                 null), effects);
                 mAlwaysOnEffects.put(alwaysOnId, alwaysOnVibration);
                 updateAlwaysOnLocked(alwaysOnVibration);
@@ -397,16 +396,16 @@
     }
 
     @Override // Binder call
-    public void vibrate(int uid, int displayId, String opPkg, @NonNull CombinedVibration effect,
+    public void vibrate(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect,
             @Nullable VibrationAttributes attrs, String reason, IBinder token) {
-        vibrateWithPermissionCheck(uid, displayId, opPkg, effect, attrs, reason, token);
+        vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
     }
 
     @Override // Binder call
     public void performHapticFeedback(
-            int uid, int displayId, String opPkg, int constant, boolean always, String reason,
+            int uid, int deviceId, String opPkg, int constant, boolean always, String reason,
             IBinder token) {
-        performHapticFeedbackInternal(uid, displayId, opPkg, constant, always, reason, token);
+        performHapticFeedbackInternal(uid, deviceId, opPkg, constant, always, reason, token);
     }
 
     /**
@@ -417,7 +416,7 @@
     @VisibleForTesting
     @Nullable
     HalVibration performHapticFeedbackInternal(
-            int uid, int displayId, String opPkg, int constant, boolean always, String reason,
+            int uid, int deviceId, String opPkg, int constant, boolean always, String reason,
             IBinder token) {
         HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider();
         if (hapticVibrationProvider == null) {
@@ -433,7 +432,7 @@
         VibrationAttributes attrs =
                 hapticVibrationProvider.getVibrationAttributesForHapticFeedback(
                         constant, /* bypassVibrationIntensitySetting= */ always);
-        return vibrateWithoutPermissionCheck(uid, displayId, opPkg, combinedVibration, attrs,
+        return vibrateWithoutPermissionCheck(uid, deviceId, opPkg, combinedVibration, attrs,
                 "performHapticFeedback: " + reason, token);
     }
 
@@ -444,7 +443,7 @@
      */
     @VisibleForTesting
     @Nullable
-    HalVibration vibrateWithPermissionCheck(int uid, int displayId, String opPkg,
+    HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
             String reason, IBinder token) {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
@@ -452,24 +451,24 @@
             attrs = fixupVibrationAttributes(attrs, effect);
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.VIBRATE, "vibrate");
-            return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
+            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
     }
 
-    HalVibration vibrateWithoutPermissionCheck(int uid, int displayId, String opPkg,
+    HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
             String reason, IBinder token) {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
         try {
-            return vibrateInternal(uid, displayId, opPkg, effect, attrs, reason, token);
+            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
     }
 
-    private HalVibration vibrateInternal(int uid, int displayId, String opPkg,
+    private HalVibration vibrateInternal(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
             String reason, IBinder token) {
         if (token == null) {
@@ -482,7 +481,7 @@
         }
         // Create Vibration.Stats as close to the received request as possible, for tracking.
         HalVibration vib = new HalVibration(token, effect,
-                new Vibration.CallerInfo(attrs, uid, displayId, opPkg, reason));
+                new Vibration.CallerInfo(attrs, uid, deviceId, opPkg, reason));
         fillVibrationFallbacks(vib, effect);
 
         if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
@@ -1558,10 +1557,9 @@
         private ExternalVibrationHolder(ExternalVibration externalVibration) {
             super(externalVibration.getToken(), new Vibration.CallerInfo(
                     externalVibration.getVibrationAttributes(), externalVibration.getUid(),
-                    // TODO(b/243604888): propagating displayID from IExternalVibration instead of
-                    //  using INVALID_DISPLAY for all external vibrations.
-                    Display.INVALID_DISPLAY,
-                    externalVibration.getPackage(), null));
+                    // TODO(b/249785241): Find a way to link ExternalVibration to a VirtualDevice
+                    // instead of using DEVICE_ID_INVALID here and relying on the UID checks.
+                    Context.DEVICE_ID_INVALID, externalVibration.getPackage(), null));
             this.externalVibration = externalVibration;
             this.scale = IExternalVibratorService.SCALE_NONE;
             mStatus = Vibration.Status.RUNNING;
@@ -1974,8 +1972,6 @@
             boolean alreadyUnderExternalControl = false;
             boolean waitForCompletion = false;
             synchronized (mLock) {
-                // TODO(b/243604888): propagating displayID from IExternalVibration instead of
-                // using INVALID_DISPLAY for all external vibrations.
                 Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationLocked(
                         vibHolder.callerInfo);
 
@@ -2184,7 +2180,7 @@
             IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
                     : mShellCallbacksToken;
             HalVibration vib = vibrateWithPermissionCheck(Binder.getCallingUid(),
-                    Display.DEFAULT_DISPLAY, SHELL_PACKAGE_NAME, combined, attrs,
+                    Context.DEVICE_ID_DEFAULT, SHELL_PACKAGE_NAME, combined, attrs,
                     commonOptions.description, deathBinder);
             maybeWaitOnVibration(vib, commonOptions);
         }
@@ -2241,7 +2237,7 @@
             IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
                     : mShellCallbacksToken;
             HalVibration vib = performHapticFeedbackInternal(Binder.getCallingUid(),
-                    Display.DEFAULT_DISPLAY, SHELL_PACKAGE_NAME, constant,
+                    Context.DEVICE_ID_DEFAULT, SHELL_PACKAGE_NAME, constant,
                     /* always= */ commonOptions.force, /* reason= */ commonOptions.description,
                     deathBinder);
             maybeWaitOnVibration(vib, commonOptions);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index a2f5a38..c2b5f88 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -583,7 +583,7 @@
                                 + " if the PI sender upgrades target_sdk to 34+! "
                                 + " (missing opt in by PI sender)! "
                                 + state.dump(resultForCaller, resultForRealCaller));
-                showBalBlockedToast("BAL would be blocked", state);
+                showBalRiskToast("BAL would be blocked", state);
                 return statsLog(resultForRealCaller, state);
             }
             Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a778415..c9703db 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4648,7 +4648,7 @@
                                 // Expanding pip into new rotation, so create a rotation leash
                                 // until the display is rotated.
                                 topActivity.getOrCreateFixedRotationLeash(
-                                        topActivity.getSyncTransaction());
+                                        topActivity.getPendingTransaction());
                             }
                             lastParentBeforePip.moveToFront("movePinnedActivityToOriginalTask");
                         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c0bf2ce..3e23fab 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1052,12 +1052,12 @@
      * @return true if we are *guaranteed* to enter-pip. This means we return false if there's
      *         a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
      */
-    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar,
-            @Nullable ActivityRecord resuming) {
+    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
         if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
             return false;
         }
 
+        final ActivityRecord resuming = getVisibleTransientLaunch(ar.getTaskDisplayArea());
         if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
             if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) {
                 // force enable pip-on-task-switch now that we've committed to actually launching
@@ -1196,9 +1196,7 @@
                 final boolean isScreenOff = ar.mDisplayContent == null
                         || ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
                 if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
-                    final ActivityRecord resuming = getVisibleTransientLaunch(
-                            ar.getTaskDisplayArea());
-                    final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming);
+                    final boolean commitVisibility = !checkEnterPipOnFinish(ar);
                     // Avoid commit visibility if entering pip or else we will get a sudden
                     // "flash" / surface going invisible for a split second.
                     if (commitVisibility) {
@@ -1431,7 +1429,7 @@
             if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
                 continue;
             }
-            if (!candidateActivity.isVisible()) {
+            if (!candidateActivity.isVisibleRequested()) {
                 continue;
             }
             return candidateActivity;
diff --git a/services/core/java/com/android/server/wm/WindowManagerFlags.java b/services/core/java/com/android/server/wm/WindowManagerFlags.java
index 4667710..b3a3650 100644
--- a/services/core/java/com/android/server/wm/WindowManagerFlags.java
+++ b/services/core/java/com/android/server/wm/WindowManagerFlags.java
@@ -49,5 +49,8 @@
 
     final boolean mWallpaperOffsetAsync = Flags.wallpaperOffsetAsync();
 
+    final boolean mAllowsScreenSizeDecoupledFromStatusBarAndCutout =
+            Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout();
+
     /* End Available Flags */
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 55678c5..0a986c8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -336,7 +336,6 @@
 import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
 import com.android.server.power.ShutdownThread;
 import com.android.server.utils.PriorityDump;
-import com.android.window.flags.Flags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -1194,7 +1193,7 @@
                 .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking);
         final boolean isScreenSizeDecoupledFromStatusBarAndCutout = context.getResources()
                 .getBoolean(R.bool.config_decoupleStatusBarAndDisplayCutoutFromScreenSize)
-                && Flags.closeToSquareConfigIncludesStatusBar();
+                && mFlags.mAllowsScreenSizeDecoupledFromStatusBarAndCutout;
         if (!isScreenSizeDecoupledFromStatusBarAndCutout) {
             mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars();
             mConfigTypes = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
diff --git a/services/proguard.flags b/services/proguard.flags
index 261bb7c..407505d 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -14,13 +14,20 @@
 }
 
 # APIs referenced by dependent JAR files and modules
--keep @interface android.annotation.SystemApi
+# TODO(b/300514883): Pull @SystemApi keep rules from system-api.pro.
+-keep interface android.annotation.SystemApi
 -keep @android.annotation.SystemApi class * {
   public protected *;
 }
 -keepclasseswithmembers class * {
   @android.annotation.SystemApi *;
 }
+# Also ensure nested classes are kept. This is overly conservative, but handles
+# cases where such classes aren't explicitly marked @SystemApi.
+-if @android.annotation.SystemApi class *
+-keep public class <1>$** {
+  public protected *;
+}
 
 # Derivatives of SystemService and other services created via reflection
 -keep,allowoptimization,allowaccessmodification class * extends com.android.server.SystemService {
diff --git a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
index df46054..1838fe8 100644
--- a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -1081,7 +1081,7 @@
         assertThat(result.powerMonitors).isNotNull();
         assertThat(Arrays.stream(result.powerMonitors).map(PowerMonitor::getName).toList())
                 .containsAtLeast(
-                        "energyconsumer0",
+                        "ENERGYCONSUMER0",
                         "BLUETOOTH/1",
                         "[channelname0]:channelsubsystem0",
                         "[channelname1]:channelsubsystem1");
@@ -1131,7 +1131,7 @@
         Map<String, PowerMonitor> map =
                 Arrays.stream(supportedPowerMonitorsResult.powerMonitors)
                         .collect(Collectors.toMap(PowerMonitor::getName, pm -> pm));
-        PowerMonitor consumer1 = map.get("energyconsumer0");
+        PowerMonitor consumer1 = map.get("ENERGYCONSUMER0");
         PowerMonitor consumer2 = map.get("BLUETOOTH/1");
         PowerMonitor measurement1 = map.get("[channelname0]:channelsubsystem0");
         PowerMonitor measurement2 = map.get("[channelname1]:channelsubsystem1");
@@ -1196,6 +1196,6 @@
         supportedPowerMonitorsResult = new GetSupportedPowerMonitorsResult();
         mService.getSupportedPowerMonitorsImpl(supportedPowerMonitorsResult);
         assertThat(Arrays.stream(supportedPowerMonitorsResult.powerMonitors)
-                .map(PowerMonitor::getName).toList()).contains("energyconsumer0");
+                .map(PowerMonitor::getName).toList()).contains("ENERGYCONSUMER0");
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 2598a6b..cdff623 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -259,8 +259,6 @@
     @Mock
     private Consumer<ArraySet<Integer>> mRunningAppsChangedCallback;
     @Mock
-    private VirtualDeviceManagerInternal.VirtualDisplayListener mDisplayListener;
-    @Mock
     private VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener mAppsOnVirtualDeviceListener;
     @Mock
     IPowerManager mIPowerManagerMock;
@@ -724,28 +722,6 @@
     }
 
     @Test
-    public void onVirtualDisplayCreatedLocked_listenersNotified() {
-        mLocalService.registerVirtualDisplayListener(mDisplayListener);
-
-        mLocalService.onVirtualDisplayCreated(DISPLAY_ID_1);
-        TestableLooper.get(this).processAllMessages();
-
-        verify(mDisplayListener).onVirtualDisplayCreated(DISPLAY_ID_1);
-    }
-
-    @Test
-    public void onVirtualDisplayRemovedLocked_listenersNotified() {
-        mLocalService.registerVirtualDisplayListener(mDisplayListener);
-
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-
-        mLocalService.onVirtualDisplayRemoved(mDeviceImpl, DISPLAY_ID_1);
-        TestableLooper.get(this).processAllMessages();
-
-        verify(mDisplayListener).onVirtualDisplayRemoved(DISPLAY_ID_1);
-    }
-
-    @Test
     public void onAppsOnVirtualDeviceChanged_singleVirtualDevice_listenersNotified() {
         ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
         mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 4406d83..ea11395 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -426,7 +426,7 @@
     }
 
     @Test
-    public void testOnPackageChanged_removingDisallowedPackage() {
+    public void testOnPackageChanged_removingPackage_removeFromDisallowed() {
         NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
         VersionedPackage a1 = new VersionedPackage("pkg1", 243);
         NotificationListenerFilter nlf2 =
@@ -440,6 +440,25 @@
 
         assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))
                 .getDisallowedPackages()).isEmpty();
+        assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0))
+                .getDisallowedPackages()).isEmpty();
+    }
+
+    @Test
+    public void testOnPackageChanged_notRemovingPackage_staysInDisallowed() {
+        NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
+        VersionedPackage a1 = new VersionedPackage("pkg1", 243);
+        NotificationListenerFilter nlf2 =
+                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
+        mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
+        mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf2);
+
+        String[] pkgs = new String[] {"pkg1"};
+        int[] uids = new int[] {243};
+        mListeners.onPackagesChanged(false, pkgs, uids);
+
+        assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0))
+                .getDisallowedPackages()).contains(a1);
     }
 
     @Test
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 6792cfe..3803244 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -43,6 +43,7 @@
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MAX;
 import static android.app.NotificationManager.IMPORTANCE_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
@@ -72,6 +73,7 @@
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
 import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -163,6 +165,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.BroadcastReceiver;
@@ -3820,6 +3823,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
 
@@ -3870,6 +3874,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(emptyList());
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
         try {
@@ -12777,6 +12782,145 @@
         verify(mSnoozeHelper).clearData(anyInt());
     }
 
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.getNotificationPolicy("package");
+
+        verify(zenHelper).getNotificationPolicyFromImplicitZenRule(eq("package"));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(),
+                eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
     private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
             throws RemoteException {
         StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 47f15b8..1e3b728 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -73,6 +74,14 @@
 public class SnoozeHelperTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "test_channel_id";
 
+    private static final String XML_TAG_NAME = "snoozed-notifications";
+    private static final String XML_SNOOZED_NOTIFICATION = "notification";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
+    private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
+    private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
+    private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
+
     @Mock SnoozeHelper.Callback mCallback;
     @Mock AlarmManager mAm;
     @Mock ManagedServices.UserProfiles mUserProfiles;
@@ -316,6 +325,53 @@
     }
 
     @Test
+    public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException {
+        final long snoozeTimeout = 1234;
+        final String snoozeContext = "ctx";
+        // Serialize & deserialize notifications so that only persisted lists are used
+        TypedXmlSerializer serializer = Xml.newFastSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        serializer.startDocument(null, true);
+        serializer.startTag(null, XML_TAG_NAME);
+        // Serialize maximum number of timed + context snoozed notifications, half of each
+        for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) {
+            final boolean timedNotification = i % 2 == 0;
+            if (timedNotification) {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+            serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1);
+            serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i);
+            if (timedNotification) {
+                serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+        }
+        serializer.endTag(null, XML_TAG_NAME);
+        serializer.endDocument();
+        serializer.flush();
+
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), "utf-8");
+        mSnoozeHelper.readXml(parser, 1);
+        // Verify that we can't snooze any more notifications
+        //  and that the limit is caused by persisted notifications
+        assertThat(mSnoozeHelper.canSnooze(1)).isFalse();
+        assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse();
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", "key0")).isEqualTo(snoozeTimeout);
+        assertThat(
+            mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                "key1")).isEqualTo(snoozeContext);
+    }
+
+    @Test
     public void testCancelByApp() throws Exception {
         NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
         NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM);
@@ -611,6 +667,7 @@
 
     @Test
     public void repostGroupSummary_repostsSummary() throws Exception {
+        final int snoozeDuration = 1000;
         IntArray profileIds = new IntArray();
         profileIds.add(UserHandle.USER_SYSTEM);
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
@@ -618,10 +675,14 @@
                 "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
         NotificationRecord r2 = getNotificationRecord(
                 "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
-        mSnoozeHelper.snooze(r, 1000);
-        mSnoozeHelper.snooze(r2, 1000);
+        final long snoozeTime = System.currentTimeMillis() + snoozeDuration;
+        mSnoozeHelper.snooze(r, snoozeDuration);
+        mSnoozeHelper.snooze(r2, snoozeDuration);
         assertEquals(2, mSnoozeHelper.getSnoozed().size());
         assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isAtLeast(snoozeTime);
 
         mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
 
@@ -630,6 +691,39 @@
 
         assertEquals(1, mSnoozeHelper.getSnoozed().size());
         assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isEqualTo(0);
+    }
+
+    @Test
+    public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception {
+        final String snoozeContext = "zzzzz";
+        IntArray profileIds = new IntArray();
+        profileIds.add(UserHandle.USER_SYSTEM);
+        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
+        NotificationRecord r = getNotificationRecord(
+                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
+        NotificationRecord r2 = getNotificationRecord(
+                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
+        mSnoozeHelper.snooze(r, snoozeContext);
+        mSnoozeHelper.snooze(r2, snoozeContext);
+        assertEquals(2, mSnoozeHelper.getSnoozed().size());
+        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+            "pkg", r.getKey())).isEqualTo(snoozeContext);
+
+        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
+
+        verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false);
+        verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false);
+
+        assertEquals(1, mSnoozeHelper.getSnoozed().size());
+        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", r.getKey())).isNull();
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 27e8f36..8f30f41 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -54,6 +54,15 @@
         return mRankingHelper;
     }
 
+    /**
+     * Sets {@link #isSystemUid} and {@link #isSystemAppId} to {@code false}, so that calls to NMS
+     * methods don't succeed {@link #isCallingUidSystem()} and similar checks.
+     */
+    void setCallerIsNormalPackage() {
+        isSystemUid = false;
+        isSystemAppId = false;
+    }
+
     @Override
     protected boolean isCallingUidSystem() {
         countSystemChecks++;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
new file mode 100644
index 0000000..6cc1c43
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.server.notification;
+
+import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenPolicy;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ZenAdaptersTest extends UiServiceTestCase {
+
+    @Test
+    public void notificationPolicyToZenPolicy_allCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_ANY, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_ANYONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_starredCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_STARRED,
+                0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_repeatCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_noCallers() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsAllowedSendersUnset() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CONVERSATIONS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsNotAllowedSendersUnset() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_setEffects() {
+        Policy policy = new Policy(0, 0, 0,
+                Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_ALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_unsetEffects() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index e8201fd..37aeb57 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -22,6 +22,7 @@
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
@@ -32,6 +33,7 @@
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY;
+import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
@@ -40,8 +42,11 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
 
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
 import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
@@ -50,6 +55,8 @@
 import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -72,6 +79,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -82,6 +90,7 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -92,6 +101,7 @@
 import android.media.AudioSystem;
 import android.media.VolumePolicy;
 import android.net.Uri;
+import android.os.Parcel;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -100,6 +110,7 @@
 import android.service.notification.Condition;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -124,6 +135,7 @@
 import com.android.server.notification.ManagedServices.UserProfiles;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.Before;
@@ -157,27 +169,29 @@
     private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
     private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
     private static final String CUSTOM_PKG_NAME = "not.android";
+    private static final String CUSTOM_APP_LABEL = "This is not Android";
     private static final int CUSTOM_PKG_UID = 1;
     private static final String CUSTOM_RULE_ID = "custom_rule";
 
-    private final String NAME = "name";
-    private final ComponentName OWNER = new ComponentName("pkg", "cls");
-    private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
-    private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
-    private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
+    private static final String NAME = "name";
+    private static final ComponentName OWNER = new ComponentName("pkg", "cls");
+    private static final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
+    private static final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
+    private static final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
             .authority("authority")
             .appendPath("path")
             .appendPath("test")
             .build();
 
-    private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE);
-    private final String TRIGGER_DESC = "Every Night, 10pm to 6am";
-    private final int TYPE = TYPE_BEDTIME;
-    private final boolean ALLOW_MANUAL = true;
-    private final int ICON_RES_ID = 1234;
-    private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
-    private final boolean ENABLED = true;
-    private final int CREATION_TIME = 123;
+    private static final Condition CONDITION = new Condition(CONDITION_ID, "",
+            Condition.STATE_TRUE);
+    private static final String TRIGGER_DESC = "Every Night, 10pm to 6am";
+    private static final int TYPE = TYPE_BEDTIME;
+    private static final boolean ALLOW_MANUAL = true;
+    private static final int ICON_RES_ID = 1234;
+    private static final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
+    private static final boolean ENABLED = true;
+    private static final int CREATION_TIME = 123;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -227,6 +241,10 @@
                 .thenReturn(CUSTOM_PKG_UID);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
                 new String[] {pkg});
+        ApplicationInfo mockAppInfo = mock(ApplicationInfo.class);
+        when(mockAppInfo.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL);
+        when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt()))
+                .thenReturn(mockAppInfo);
         mZenModeHelper.mPm = mPackageManager;
 
         mZenModeEventLogger.reset();
@@ -334,7 +352,7 @@
 
     @Test
     public void testZenOff_NoMuteApplied() {
-        mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
@@ -635,7 +653,7 @@
 
         // 3. apply zen off - verify zen is set to previous ringer (normal)
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -721,7 +739,7 @@
 
         // 3.  apply zen off - verify ringer remains normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -746,7 +764,7 @@
 
         // 3. apply zen-off - verify ringer is still silent
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -781,7 +799,7 @@
 
         // 4.  apply zen off - verify ringer still silenced
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -795,7 +813,7 @@
 
         // apply zen off multiple times - verify ringer is not set to normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = null; // will evaluate config to zen mode off
         for (int i = 0; i < 3; i++) {
             // if zen doesn't change, zen should not reapply itself to the ringer
@@ -809,7 +827,7 @@
     public void testSilentRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -836,7 +854,7 @@
     public void testVibrateRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -1209,7 +1227,7 @@
                 .allowMedia(false)
                 .allowRepeatCallers(false)
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)
-                .allowMessages(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+                .allowMessages(PEOPLE_TYPE_CONTACTS)
                 .allowEvents(true)
                 .allowReminders(false)
                 .build();
@@ -2023,10 +2041,10 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode);
 
         // and also that it works to turn it back off again
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
     @Test
@@ -2041,7 +2059,7 @@
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
                 false);
 
         // In total, this should be 2 loggable changes
@@ -2060,7 +2078,7 @@
         //   - resulting DNDPolicyProto the same as the values in setupZenConfig()
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2080,7 +2098,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
@@ -2144,7 +2162,7 @@
         //   - zen policy is the same as the set-up zen config
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2157,7 +2175,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertTrue(mZenModeEventLogger.getIsUserAction(1));
@@ -2201,7 +2219,7 @@
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
         // Change the policy again
@@ -2305,7 +2323,7 @@
         // what the event should reflect. At this time, the policy is the same as initial setup.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
@@ -2355,7 +2373,7 @@
         mZenModeHelper.evaluateZenModeLocked("test", true);
 
         // Check that the change actually took: zen mode should be off now
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
 
         // but still, nothing should've been logged
         assertEquals(0, mZenModeEventLogger.numLoggedChanges());
@@ -2483,7 +2501,7 @@
                 true);
 
         // Turn off manual mode, call from a package: don't reset UID even though enabler is set
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null,
                 CUSTOM_PKG_NAME, "", 12345, false);
 
         // And likewise when turning it back on again
@@ -2660,8 +2678,11 @@
     }
 
     @Test
-    public void testCreateAutomaticZenRule_allFields() {
+    public void zenRuleToAutomaticZenRule_allFields() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
+                new String[] {OWNER.getPackageName()});
+
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = CONFIG_ACTIVITY;
         rule.component = OWNER;
@@ -2682,7 +2703,8 @@
         rule.iconResId = ICON_RES_ID;
         rule.triggerDescription = TRIGGER_DESC;
 
-        AutomaticZenRule actual = mZenModeHelper.createAutomaticZenRule(rule);
+        mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
+        AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(rule.id);
 
         assertEquals(NAME, actual.getName());
         assertEquals(OWNER, actual.getOwner());
@@ -2915,8 +2937,253 @@
         assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
     }
 
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_FALSE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID,
+                        ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_CONTACTS)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy original = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                original);
+
+        // Change priorityCallSenders: contacts -> starred.
+        Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_STARRED)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID, new Policy(0, 0, 0)));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
+                CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                writtenPolicy);
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isEqualTo(writtenPolicy);
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS =
+            Correspondence.transforming(zr -> {
+                Parcel p = Parcel.obtain();
+                try {
+                    zr.writeToParcel(p, 0);
+                    p.setDataPosition(0);
+                    ZenRule copy = new ZenRule(p);
+                    copy.creationTime = 0;
+                    return copy;
+                } finally {
+                    p.recycle();
+                }
+            },
+            "Ignoring timestamps");
+
+    private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
+            @Nullable Boolean conditionActive) {
+        ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.id = "implicit_" + ownerPkg;
+        rule.conditionId = Uri.parse("condition://android/implicit/" + ownerPkg);
+        if (conditionActive != null) {
+            rule.condition = conditionActive
+                    ? new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
+                    : new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            STATE_FALSE);
+        }
+        rule.zenMode = zenMode;
+        rule.zenPolicy = policy;
+        rule.pkg = ownerPkg;
+        rule.name = CUSTOM_APP_LABEL;
+        rule.enabled = true;
+        return rule;
+    }
+
     private void setupZenConfig() {
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig.allowAlarms = false;
         mZenModeHelper.mConfig.allowMedia = false;
         mZenModeHelper.mConfig.allowSystem = false;
@@ -2965,6 +3232,15 @@
         assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber());
     }
 
+    private static void withoutWtfCrash(Runnable test) {
+        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {});
+        try {
+            test.run();
+        } finally {
+            Log.setWtfHandler(oldHandler);
+        }
+    }
+
     /**
      * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml()
      */
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index 7a2bb5a..f080341 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -75,8 +75,6 @@
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
-import android.util.ArraySet;
-import android.view.Display;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -103,7 +101,7 @@
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final int UID = 1;
-    private static final int VIRTUAL_DISPLAY_ID = 1;
+    private static final int VIRTUAL_DEVICE_ID = 1;
     private static final String SYSUI_PACKAGE_NAME = "sysui";
     private static final PowerSaveState NORMAL_POWER_STATE = new PowerSaveState.Builder().build();
     private static final PowerSaveState LOW_POWER_STATE = new PowerSaveState.Builder()
@@ -137,9 +135,6 @@
     private VibrationSettings mVibrationSettings;
     private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
     private BroadcastReceiver mRegisteredBatteryBroadcastReceiver;
-    private VirtualDeviceManagerInternal.VirtualDisplayListener mRegisteredVirtualDisplayListener;
-    private VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener
-            mRegisteredAppsOnVirtualDeviceListener;
 
     @Before
     public void setUp() throws Exception {
@@ -155,14 +150,6 @@
         }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any());
         when(mPackageManagerInternalMock.getSystemUiServiceComponent())
                 .thenReturn(new ComponentName(SYSUI_PACKAGE_NAME, ""));
-        doAnswer(invocation -> {
-            mRegisteredVirtualDisplayListener = invocation.getArgument(0);
-            return null;
-        }).when(mVirtualDeviceManagerInternalMock).registerVirtualDisplayListener(any());
-        doAnswer(invocation -> {
-            mRegisteredAppsOnVirtualDeviceListener = invocation.getArgument(0);
-            return null;
-        }).when(mVirtualDeviceManagerInternalMock).registerAppsOnVirtualDeviceListener(any());
 
         removeServicesForTest();
         addServicesForTest();
@@ -654,62 +641,20 @@
     }
 
     @Test
-    public void shouldIgnoreVibrationFromVirtualDisplays_displayNonVirtual_neverIgnored() {
-        // Vibrations from the primary display is never ignored regardless of the creation and
-        // removal of virtual displays and of the changes of apps running on virtual displays.
-        mRegisteredVirtualDisplayListener.onVirtualDisplayCreated(VIRTUAL_DISPLAY_ID);
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(
-                new ArraySet<>(Arrays.asList(UID)));
+    public void shouldIgnoreVibrationFromVirtualDevices_defaultDevice_neverIgnored() {
+        // Vibrations from the primary device is never ignored.
         for (int usage : ALL_USAGES) {
-            assertVibrationNotIgnoredForUsageAndDisplay(usage, Display.DEFAULT_DISPLAY);
-        }
-
-        mRegisteredVirtualDisplayListener.onVirtualDisplayRemoved(VIRTUAL_DISPLAY_ID);
-        for (int usage : ALL_USAGES) {
-            assertVibrationNotIgnoredForUsageAndDisplay(usage, Display.DEFAULT_DISPLAY);
-        }
-
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(new ArraySet<>());
-        for (int usage : ALL_USAGES) {
-            assertVibrationNotIgnoredForUsageAndDisplay(usage, Display.DEFAULT_DISPLAY);
+            assertVibrationNotIgnoredForUsageAndDevice(usage, Context.DEVICE_ID_DEFAULT);
         }
     }
 
     @Test
-    public void shouldIgnoreVibrationFromVirtualDisplays_displayVirtual() {
-        // Ignore the vibration when the coming display id represents a virtual display.
-        mRegisteredVirtualDisplayListener.onVirtualDisplayCreated(VIRTUAL_DISPLAY_ID);
-
+    public void shouldIgnoreVibrationFromVirtualDevices_virtualDevice_alwaysIgnored() {
+        // Ignore the vibration when the coming device id represents a virtual device.
         for (int usage : ALL_USAGES) {
-            assertVibrationIgnoredForUsageAndDisplay(usage, VIRTUAL_DISPLAY_ID,
+            assertVibrationIgnoredForUsageAndDevice(usage, VIRTUAL_DEVICE_ID,
                     Vibration.Status.IGNORED_FROM_VIRTUAL_DEVICE);
         }
-
-        // Stop ignoring when the virtual display is removed.
-        mRegisteredVirtualDisplayListener.onVirtualDisplayRemoved(VIRTUAL_DISPLAY_ID);
-        for (int usage : ALL_USAGES) {
-            assertVibrationNotIgnoredForUsageAndDisplay(usage, VIRTUAL_DISPLAY_ID);
-        }
-    }
-
-
-    @Test
-    public void shouldIgnoreVibrationFromVirtualDisplays_appsOnVirtualDisplay() {
-        // Ignore when the passed-in display id is invalid and the calling uid is on a virtual
-        // display.
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(
-                new ArraySet<>(Arrays.asList(UID)));
-        for (int usage : ALL_USAGES) {
-            assertVibrationIgnoredForUsageAndDisplay(usage, Display.INVALID_DISPLAY,
-                    Vibration.Status.IGNORED_FROM_VIRTUAL_DEVICE);
-        }
-
-        // Stop ignoring when the app is no longer on virtual display.
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(new ArraySet<>());
-        for (int usage : ALL_USAGES) {
-            assertVibrationNotIgnoredForUsageAndDisplay(usage, Display.INVALID_DISPLAY);
-        }
-
     }
 
     @Test
@@ -932,13 +877,13 @@
 
     private void assertVibrationIgnoredForUsage(@VibrationAttributes.Usage int usage,
             Vibration.Status expectedStatus) {
-        assertVibrationIgnoredForUsageAndDisplay(usage, Display.DEFAULT_DISPLAY, expectedStatus);
+        assertVibrationIgnoredForUsageAndDevice(usage, Context.DEVICE_ID_DEFAULT, expectedStatus);
     }
 
-    private void assertVibrationIgnoredForUsageAndDisplay(@VibrationAttributes.Usage int usage,
-            int displayId, Vibration.Status expectedStatus) {
+    private void assertVibrationIgnoredForUsageAndDevice(@VibrationAttributes.Usage int usage,
+            int deviceId, Vibration.Status expectedStatus) {
         Vibration.CallerInfo callerInfo = new Vibration.CallerInfo(
-                VibrationAttributes.createForUsage(usage), UID, displayId, null, null);
+                VibrationAttributes.createForUsage(usage), UID, deviceId, null, null);
         assertEquals(errorMessageForUsage(usage), expectedStatus,
                 mVibrationSettings.shouldIgnoreVibration(callerInfo));
     }
@@ -946,7 +891,7 @@
     private void assertVibrationIgnoredForAttributes(VibrationAttributes attrs,
             Vibration.Status expectedStatus) {
         Vibration.CallerInfo callerInfo = new Vibration.CallerInfo(attrs, UID,
-                Display.DEFAULT_DISPLAY, null, null);
+                Context.DEVICE_ID_DEFAULT, null, null);
         assertEquals(errorMessageForAttributes(attrs), expectedStatus,
                 mVibrationSettings.shouldIgnoreVibration(callerInfo));
     }
@@ -957,27 +902,27 @@
 
     private void assertVibrationNotIgnoredForUsageAndFlags(@VibrationAttributes.Usage int usage,
             @VibrationAttributes.Flag int flags) {
-        assertVibrationNotIgnoredForUsageAndFlagsAndDisplay(usage, Display.DEFAULT_DISPLAY, flags);
+        assertVibrationNotIgnoredForUsageAndFlagsAndDevice(usage, Context.DEVICE_ID_DEFAULT, flags);
     }
 
-    private void assertVibrationNotIgnoredForUsageAndDisplay(@VibrationAttributes.Usage int usage,
-            int displayId) {
-        assertVibrationNotIgnoredForUsageAndFlagsAndDisplay(usage, displayId, /* flags= */ 0);
+    private void assertVibrationNotIgnoredForUsageAndDevice(@VibrationAttributes.Usage int usage,
+            int deviceId) {
+        assertVibrationNotIgnoredForUsageAndFlagsAndDevice(usage, deviceId, /* flags= */ 0);
     }
 
-    private void assertVibrationNotIgnoredForUsageAndFlagsAndDisplay(
-            @VibrationAttributes.Usage int usage, int displayId,
+    private void assertVibrationNotIgnoredForUsageAndFlagsAndDevice(
+            @VibrationAttributes.Usage int usage, int deviceId,
             @VibrationAttributes.Flag int flags) {
         Vibration.CallerInfo callerInfo = new Vibration.CallerInfo(
                 new VibrationAttributes.Builder().setUsage(usage).setFlags(flags).build(), UID,
-                displayId, null, null);
+                deviceId, null, null);
         assertNull(errorMessageForUsage(usage),
                 mVibrationSettings.shouldIgnoreVibration(callerInfo));
     }
 
     private void assertVibrationNotIgnoredForAttributes(VibrationAttributes attrs) {
         Vibration.CallerInfo callerInfo = new Vibration.CallerInfo(attrs, UID,
-                Display.DEFAULT_DISPLAY, null, null);
+                Context.DEVICE_ID_DEFAULT, null, null);
         assertNull(errorMessageForAttributes(attrs),
                 mVibrationSettings.shouldIgnoreVibration(callerInfo));
     }
@@ -1032,7 +977,7 @@
     private Vibration.CallerInfo createCallerInfo(int uid, String opPkg,
             @VibrationAttributes.Usage int usage) {
         VibrationAttributes attrs = VibrationAttributes.createForUsage(usage);
-        return new Vibration.CallerInfo(attrs, uid, VIRTUAL_DISPLAY_ID, opPkg, null);
+        return new Vibration.CallerInfo(attrs, uid, VIRTUAL_DEVICE_ID, opPkg, null);
     }
 
     private void setBatteryReceiverRegistrationResult(Intent result) {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 085241f..b0aef47 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -88,7 +88,7 @@
 
     private static final int TEST_TIMEOUT_MILLIS = 900;
     private static final int UID = Process.ROOT_UID;
-    private static final int DISPLAY_ID = 10;
+    private static final int DEVICE_ID = 10;
     private static final int VIBRATOR_ID = 1;
     private static final String PACKAGE_NAME = "package";
     private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
@@ -250,7 +250,7 @@
         Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo(
                 Vibration.Status.CANCELLED_SUPERSEDED, new Vibration.CallerInfo(
                 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ALARM), /* uid= */
-                1, /* displayId= */ -1, /* opPkg= */ null, /* reason= */ null));
+                1, /* deviceId= */ -1, /* opPkg= */ null, /* reason= */ null));
         mVibrationConductor.notifyCancelled(
                 cancelVibrationInfo,
                 /* immediate= */ false);
@@ -1641,7 +1641,7 @@
 
     private HalVibration createVibration(CombinedVibration effect) {
         return new HalVibration(mVibrationToken, effect,
-                new Vibration.CallerInfo(ATTRS, UID, DISPLAY_ID, PACKAGE_NAME, "reason"));
+                new Vibration.CallerInfo(ATTRS, UID, DEVICE_ID, PACKAGE_NAME, "reason"));
     }
 
     private SparseArray<VibratorController> createVibratorControllers() {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 3dfaed6..3fce9e7 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -84,10 +84,8 @@
 import android.os.vibrator.VibrationEffectSegment;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
-import android.util.ArraySet;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.view.Display;
 import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
 import android.view.flags.Flags;
@@ -129,7 +127,7 @@
     // be cancelled in the body of the individual test.
     private static final int CLEANUP_TIMEOUT_MILLIS = 100;
     private static final int UID = Process.ROOT_UID;
-    private static final int VIRTUAL_DISPLAY_ID = 1;
+    private static final int VIRTUAL_DEVICE_ID = 1;
     private static final String PACKAGE_NAME = "package";
     private static final PowerSaveState NORMAL_POWER_STATE = new PowerSaveState.Builder().build();
     private static final PowerSaveState LOW_POWER_STATE = new PowerSaveState.Builder()
@@ -190,9 +188,6 @@
     private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
     private VibratorManagerService.ExternalVibratorService mExternalVibratorService;
     private VibrationConfig mVibrationConfig;
-    private VirtualDeviceManagerInternal.VirtualDisplayListener mRegisteredVirtualDisplayListener;
-    private VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener
-            mRegisteredAppsOnVirtualDeviceListener;
     private InputManagerGlobal.TestSession mInputManagerGlobalSession;
     private InputManager mInputManager;
 
@@ -223,14 +218,6 @@
             mRegisteredPowerModeListener = invocation.getArgument(0);
             return null;
         }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any());
-        doAnswer(invocation -> {
-            mRegisteredVirtualDisplayListener = invocation.getArgument(0);
-            return null;
-        }).when(mVirtualDeviceManagerInternalMock).registerVirtualDisplayListener(any());
-        doAnswer(invocation -> {
-            mRegisteredAppsOnVirtualDeviceListener = invocation.getArgument(0);
-            return null;
-        }).when(mVirtualDeviceManagerInternalMock).registerAppsOnVirtualDeviceListener(any());
 
         setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_ENABLED, 1);
@@ -273,6 +260,7 @@
 
         LocalServices.removeServiceForTest(PackageManagerInternal.class);
         LocalServices.removeServiceForTest(PowerManagerInternal.class);
+        LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
         // Ignore potential exceptions about the looper having never dispatched any messages.
         mTestLooper.stopAutoDispatchAndIgnoreExceptions();
         if (mInputManagerGlobalSession != null) {
@@ -1510,65 +1498,33 @@
     }
 
     @Test
-    public void vibrate_withVirtualDisplayChange_ignoreVibrationFromVirtualDisplay()
-            throws Exception {
+    public void vibrate_ignoreVibrationFromVirtualDevice() throws Exception {
         mockVibrators(1);
         VibratorManagerService service = createSystemReadyService();
-        mRegisteredVirtualDisplayListener.onVirtualDisplayCreated(VIRTUAL_DISPLAY_ID);
 
-        vibrateWithDisplay(service,
-                VIRTUAL_DISPLAY_ID,
+        vibrateWithDevice(service,
+                VIRTUAL_DEVICE_ID,
                 CombinedVibration.startParallel()
                         .addVibrator(1, VibrationEffect.createOneShot(1000, 100))
                         .combine(),
                 HAPTIC_FEEDBACK_ATTRS);
 
-        // Haptic feedback ignored when it's from a virtual display.
+        // Haptic feedback ignored when it's from a virtual device.
         assertFalse(waitUntil(s -> s.isVibrating(1), service, /* timeout= */ 50));
 
-        mRegisteredVirtualDisplayListener.onVirtualDisplayRemoved(VIRTUAL_DISPLAY_ID);
-        vibrateWithDisplay(service,
-                VIRTUAL_DISPLAY_ID,
+        vibrateWithDevice(service,
+                Context.DEVICE_ID_DEFAULT,
                 CombinedVibration.startParallel()
                         .addVibrator(1, VibrationEffect.createOneShot(1000, 100))
                         .combine(),
                 HAPTIC_FEEDBACK_ATTRS);
-        // Haptic feedback played normally when the virtual display is removed.
+        // Haptic feedback played normally when it's from the default device.
         assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
 
         cancelVibrate(service);  // Clean up long-ish effect.
     }
 
     @Test
-    public void vibrate_withAppsOnVirtualDisplayChange_ignoreVibrationFromVirtualDisplay()
-            throws Exception {
-        mockVibrators(1);
-        VibratorManagerService service = createSystemReadyService();
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(
-                new ArraySet<>(Arrays.asList(UID)));
-        vibrateWithDisplay(service,
-                Display.INVALID_DISPLAY,
-                CombinedVibration.startParallel()
-                        .addVibrator(1, VibrationEffect.createOneShot(1000, 100))
-                        .combine(),
-                HAPTIC_FEEDBACK_ATTRS);
-
-        // Haptic feedback ignored when it's from an app running virtual display.
-        assertFalse(waitUntil(s -> s.isVibrating(1), service, /* timeout= */ 50));
-
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(new ArraySet<>());
-        vibrateWithDisplay(service,
-                Display.INVALID_DISPLAY,
-                CombinedVibration.startParallel()
-                        .addVibrator(1, VibrationEffect.createOneShot(1000, 100))
-                        .combine(),
-                HAPTIC_FEEDBACK_ATTRS);
-        // Haptic feedback played normally when the same app no long runs on a virtual display.
-        assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
-        cancelVibrate(service);  // Clean up long-ish effect.
-    }
-
-    @Test
     public void vibrate_prebakedAndComposedVibrationsWithFallbacks_playsFallbackOnlyForPredefined()
             throws Exception {
         mockVibrators(1);
@@ -1685,7 +1641,7 @@
     }
 
     @Test
-    public void onExternalVibration_ignoreVibrationFromVirtualDevices() throws Exception {
+    public void onExternalVibration_ignoreVibrationFromVirtualDevices() {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
         createSystemReadyService();
@@ -1697,8 +1653,8 @@
         int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
         assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale);
 
-        mRegisteredAppsOnVirtualDeviceListener.onAppsOnAnyVirtualDeviceChanged(
-                new ArraySet<>(Arrays.asList(UID)));
+        when(mVirtualDeviceManagerInternalMock.isAppRunningOnAnyVirtualDevice(UID))
+                .thenReturn(true);
         scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
         assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
     }
@@ -2396,7 +2352,7 @@
     private HalVibration performHapticFeedbackAndWaitUntilFinished(VibratorManagerService service,
                 int constant, boolean always) throws InterruptedException {
         HalVibration vib =
-                service.performHapticFeedbackInternal(UID, Display.DEFAULT_DISPLAY, PACKAGE_NAME,
+                service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT, PACKAGE_NAME,
                         constant, always, "some reason", service);
         if (vib != null) {
             vib.waitForEnd();
@@ -2414,7 +2370,7 @@
     private HalVibration vibrateAndWaitUntilFinished(VibratorManagerService service,
             CombinedVibration effect, VibrationAttributes attrs) throws InterruptedException {
         HalVibration vib =
-                service.vibrateWithPermissionCheck(UID, Display.DEFAULT_DISPLAY, PACKAGE_NAME,
+                service.vibrateWithPermissionCheck(UID, Context.DEVICE_ID_DEFAULT, PACKAGE_NAME,
                         effect, attrs, "some reason", service);
         if (vib != null) {
             vib.waitForEnd();
@@ -2430,12 +2386,12 @@
 
     private void vibrate(VibratorManagerService service, CombinedVibration effect,
             VibrationAttributes attrs) {
-        vibrateWithDisplay(service, Display.DEFAULT_DISPLAY, effect, attrs);
+        vibrateWithDevice(service, Context.DEVICE_ID_DEFAULT, effect, attrs);
     }
 
-    private void vibrateWithDisplay(VibratorManagerService service, int displayId,
+    private void vibrateWithDevice(VibratorManagerService service, int deviceId,
             CombinedVibration effect, VibrationAttributes attrs) {
-        service.vibrate(UID, displayId, PACKAGE_NAME, effect, attrs, "some reason", service);
+        service.vibrate(UID, deviceId, PACKAGE_NAME, effect, attrs, "some reason", service);
     }
 
     private boolean waitUntil(Predicate<VibratorManagerService> predicate,
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index f64ab22..63de41f 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -206,6 +206,7 @@
     static final int MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED = 9;
     static final int MSG_UID_REMOVED = 10;
     static final int MSG_USER_STARTED = 11;
+    static final int MSG_NOTIFY_USAGE_EVENT_LISTENER = 12;
 
     private final Object mLock = new Object();
     private Handler mHandler;
@@ -315,6 +316,16 @@
                 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                 return true;
             }
+            case MSG_NOTIFY_USAGE_EVENT_LISTENER: {
+                final int userId = msg.arg1;
+                final Event event = (Event) msg.obj;
+                synchronized (mUsageEventListeners) {
+                    final int size = mUsageEventListeners.size();
+                    for (int i = 0; i < size; ++i) {
+                        mUsageEventListeners.valueAt(i).onUsageEvent(userId, event);
+                    }
+                }
+            }
         }
         return false;
     };
@@ -532,9 +543,6 @@
             }
             reportEvent(unlockEvent, userId);
 
-            mIoHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK,
-                    userId, 0).sendToTarget();
-
             // Remove all the stats stored in system DE.
             deleteRecursively(new File(Environment.getDataSystemDeDirectory(userId), "usagestats"));
 
@@ -546,6 +554,8 @@
                 userService.persistActiveStats();
             }
         }
+
+        mIoHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK, userId, 0).sendToTarget();
     }
 
     /**
@@ -1240,12 +1250,7 @@
             service.reportEvent(event);
         }
 
-        synchronized (mUsageEventListeners) {
-            final int size = mUsageEventListeners.size();
-            for (int i = 0; i < size; ++i) {
-                mUsageEventListeners.valueAt(i).onUsageEvent(userId, event);
-            }
-        }
+        mIoHandler.obtainMessage(MSG_NOTIFY_USAGE_EVENT_LISTENER, userId, 0, event).sendToTarget();
     }
 
     private String getUsageSourcePackage(Event event) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index bb6cf52..fb18375 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -52,9 +52,12 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_UNEXPECTED_CALLBACK;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__TRAINING_DATA;
-import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__TRAINING_DATA_SECURITY_EXCEPTION;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__TRAINING_DATA_EGRESS_LIMIT_REACHED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__TRAINING_DATA_REMOTE_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__TRAINING_DATA_SECURITY_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_DETECTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_REJECTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_TRAINING_DATA;
 import static com.android.server.voiceinteraction.HotwordDetectionConnection.ENFORCE_HOTWORD_PHRASE_ID;
 
 import android.annotation.NonNull;
@@ -73,7 +76,9 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IRemoteCallback;
+import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.SharedMemory;
@@ -94,6 +99,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.infra.AndroidFuture;
+import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
 import com.android.server.policy.AppOpsPolicy;
 import com.android.server.voiceinteraction.VoiceInteractionManagerServiceImpl.DetectorRemoteExceptionListener;
@@ -180,6 +186,13 @@
     private static final int METRICS_CALLBACK_ON_STATUS_REPORTED_EXCEPTION =
             HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_STATUS_REPORTED_EXCEPTION;
 
+    private static final int HOTWORD_EVENT_TYPE_DETECTION =
+            HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_DETECTION;
+    private static final int HOTWORD_EVENT_TYPE_REJECTION =
+            HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_REJECTION;
+    private static final int HOTWORD_EVENT_TYPE_TRAINING_DATA =
+            HOTWORD_EVENT_EGRESS_SIZE__EVENT_TYPE__HOTWORD_TRAINING_DATA;
+
     private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
     // TODO: This may need to be a Handler(looper)
     final ScheduledExecutorService mScheduledExecutorService;
@@ -516,6 +529,7 @@
                                         if (result != null) {
                                             Slog.i(TAG, "Egressed 'hotword rejected result' "
                                                     + "from hotword trusted process");
+                                            logEgressSizeStats(result);
                                             if (mDebugHotwordLogging) {
                                                 Slog.i(TAG, "Egressed detected result: " + result);
                                             }
@@ -608,6 +622,7 @@
                                         Slog.i(TAG, "Egressed "
                                                 + HotwordDetectedResult.getUsageSize(newResult)
                                                 + " bits from hotword trusted process");
+                                        logEgressSizeStats(newResult);
                                         if (mDebugHotwordLogging) {
                                             Slog.i(TAG,
                                                     "Egressed detected result: " + newResult);
@@ -624,6 +639,32 @@
                 mVoiceInteractionServiceUid);
     }
 
+    void logEgressSizeStats(HotwordTrainingData data) {
+        logEgressSizeStats(data, HOTWORD_EVENT_TYPE_TRAINING_DATA);
+    }
+
+    void logEgressSizeStats(HotwordDetectedResult data) {
+        logEgressSizeStats(data, HOTWORD_EVENT_TYPE_DETECTION);
+
+    }
+
+    void logEgressSizeStats(HotwordRejectedResult data) {
+        logEgressSizeStats(data, HOTWORD_EVENT_TYPE_REJECTION);
+    }
+
+    /** Logs event size stats for events egressed from trusted hotword detection service. */
+    private void logEgressSizeStats(Parcelable data, int eventType) {
+        BackgroundThread.getExecutor().execute(() -> {
+            Parcel parcel = Parcel.obtain();
+            parcel.writeValue(data);
+            int dataSizeBytes = parcel.dataSize();
+            parcel.recycle();
+
+            HotwordMetricsLogger.writeHotwordDataEgressSize(eventType, dataSizeBytes,
+                    getDetectorType(), mVoiceInteractionServiceUid);
+        });
+    }
+
     /** Used to send training data.
      *
      * @hide
@@ -723,6 +764,7 @@
                     mVoiceInteractionServiceUid);
             throw e;
         }
+        logEgressSizeStats(data);
     }
 
     void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
index 6418f3e..2938a58 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
@@ -186,6 +186,7 @@
                     if (mDebugHotwordLogging) {
                         Slog.i(TAG, "Egressed detected result: " + newResult);
                     }
+                    logEgressSizeStats(newResult);
                 }
             }
 
@@ -228,6 +229,7 @@
                     if (mDebugHotwordLogging && result != null) {
                         Slog.i(TAG, "Egressed rejected result: " + result);
                     }
+                    logEgressSizeStats(result);
                 }
             }
 
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
index f7b66a2..ca72c85 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
@@ -34,6 +34,9 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__NORMAL_DETECTOR;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
 import static com.android.internal.util.LatencyTracker.ACTION_SHOW_VOICE_INTERACTION;
 
 import android.content.Context;
@@ -120,6 +123,16 @@
     }
 
     /**
+     * Logs hotword event egress size metrics.
+     */
+    public static void writeHotwordDataEgressSize(int eventType, long eventSize, int detectorType,
+            int uid) {
+        int metricsDetectorType = getHotwordEventEgressSizeDetectorType(detectorType);
+        FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_EGRESS_SIZE_ATOM_REPORTED,
+                eventType, eventSize, metricsDetectorType, uid);
+    }
+
+    /**
      * Starts a {@link LatencyTracker} log for the time it takes to show the
      * {@link android.service.voice.VoiceInteractionSession} system UI after a voice trigger.
      *
@@ -224,4 +237,15 @@
                 return AUDIO_EGRESS_NORMAL_DETECTOR;
         }
     }
+
+    private static int getHotwordEventEgressSizeDetectorType(int detectorType) {
+        switch (detectorType) {
+            case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
+                return HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
+            case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
+                return HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
+            default:
+                return HOTWORD_EVENT_EGRESS_SIZE__DETECTOR_TYPE__NORMAL_DETECTOR;
+        }
+    }
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index 2e23eff..9de7f9a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
@@ -179,6 +179,7 @@
                     }
                     Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult)
                             + " bits from hotword trusted process");
+                    logEgressSizeStats(newResult);
                     if (mDebugHotwordLogging) {
                         Slog.i(TAG, "Egressed detected result: " + newResult);
                     }
@@ -194,6 +195,7 @@
                         HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
                         HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED,
                         mVoiceInteractionServiceUid);
+                logEgressSizeStats(result);
                 // onRejected isn't allowed here, and we are not expecting it.
             }
 
diff --git a/telephony/OWNERS b/telephony/OWNERS
index 3158ad8..287aa65 100644
--- a/telephony/OWNERS
+++ b/telephony/OWNERS
@@ -7,10 +7,12 @@
 tgunn@google.com
 huiwang@google.com
 jayachandranc@google.com
-chinmayd@google.com
 amruthr@google.com
 sasindran@google.com
 
 # Requiring TL ownership for new carrier config keys.
 per-file CarrierConfigManager.java=set noparent
 per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com
+
+#Domain Selection is jointly owned, add additional owners for domain selection specific files
+per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index 4b1a726..3e87872 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -951,8 +951,8 @@
      * See 3GPP TS 23.501 section 5.6.13
      *
      * @return True if the PDU session for this APN should always be on and false otherwise
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
     public boolean isAlwaysOn() {
         return mAlwaysOn;
     }
@@ -2282,9 +2282,9 @@
          * See 3GPP TS 23.501 section 5.6.13
          *
          * @param alwaysOn the always on status to set for this APN
-         * @hide
          */
-        public Builder setAlwaysOn(boolean alwaysOn) {
+        @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
+        public @NonNull Builder setAlwaysOn(boolean alwaysOn) {
             this.mAlwaysOn = alwaysOn;
             return this;
         }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 15a20cb..4c53f8a 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3147,4 +3147,16 @@
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
     void unregisterForSatelliteCapabilitiesChanged(int subId,
             in ISatelliteCapabilitiesCallback callback);
+
+    /**
+     * This API can be used by only CTS to override the cached value for the device overlay config
+     * value : config_send_satellite_datagram_to_modem_in_demo_mode, which determines whether
+     * outgoing satellite datagrams should be sent to modem in demo mode.
+     *
+     * @param shouldSendToDemoMode Whether send datagram in demo mode should be sent to satellite
+     * modem or not.
+     *
+     * @return {@code true} if the operation is successful, {@code false} otherwise.
+     */
+    boolean setShouldSendDatagramToModemInDemoMode(boolean shouldSendToModemInDemoMode);
 }
diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
index 421ceb7..07b7338 100644
--- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
+++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
@@ -50,7 +50,7 @@
 public class VibratorManagerServicePermissionTest {
 
     private static final String PACKAGE_NAME = "com.android.framework.permission.tests";
-    private static final int DISPLAY_ID = 1;
+    private static final int DEVICE_ID = 1;
     private static final CombinedVibration EFFECT =
             CombinedVibration.createParallel(
                     VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
@@ -107,7 +107,7 @@
     @Test
     public void testVibrateWithoutPermissionFails() throws RemoteException {
         expectSecurityException("VIBRATE");
-        mVibratorService.vibrate(Process.myUid(), DISPLAY_ID, PACKAGE_NAME, EFFECT, ATTRS,
+        mVibratorService.vibrate(Process.myUid(), DEVICE_ID, PACKAGE_NAME, EFFECT, ATTRS,
                 "testVibrate",
                 new Binder());
     }
@@ -117,7 +117,7 @@
             throws RemoteException {
         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 Manifest.permission.VIBRATE);
-        mVibratorService.vibrate(Process.myUid(), DISPLAY_ID, PACKAGE_NAME, EFFECT, ATTRS,
+        mVibratorService.vibrate(Process.myUid(), DEVICE_ID, PACKAGE_NAME, EFFECT, ATTRS,
                 "testVibrate",
                 new Binder());
     }
@@ -127,7 +127,7 @@
         expectSecurityException("UPDATE_APP_OPS_STATS");
         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 Manifest.permission.VIBRATE);
-        mVibratorService.vibrate(Process.SYSTEM_UID, DISPLAY_ID, "android", EFFECT, ATTRS,
+        mVibratorService.vibrate(Process.SYSTEM_UID, DEVICE_ID, "android", EFFECT, ATTRS,
                 "testVibrate",
                 new Binder());
     }
@@ -137,7 +137,7 @@
         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 Manifest.permission.VIBRATE,
                 Manifest.permission.UPDATE_APP_OPS_STATS);
-        mVibratorService.vibrate(Process.SYSTEM_UID, DISPLAY_ID, "android", EFFECT, ATTRS,
+        mVibratorService.vibrate(Process.SYSTEM_UID, DEVICE_ID, "android", EFFECT, ATTRS,
                 "testVibrate",
                 new Binder());
     }
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 159c6fd..c638873 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2514,6 +2514,28 @@
     }
   }
 
+  // Parse the feature flag values. An argument that starts with '@' points to a file to read flag
+  // values from.
+  std::vector<std::string> all_feature_flags_args;
+  for (const std::string& arg : feature_flags_args_) {
+    if (util::StartsWith(arg, "@")) {
+      const std::string path = arg.substr(1, arg.size() - 1);
+      std::string error;
+      if (!file::AppendArgsFromFile(path, &all_feature_flags_args, &error)) {
+        context.GetDiagnostics()->Error(android::DiagMessage(path) << error);
+        return 1;
+      }
+    } else {
+      all_feature_flags_args.push_back(arg);
+    }
+  }
+
+  for (const std::string& arg : all_feature_flags_args) {
+    if (ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) {
+      return 1;
+    }
+  }
+
   if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path_) {
     if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path_.value(),
         &options_.stable_id_map)) {
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index a08f385..26713fd 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -17,11 +17,17 @@
 #ifndef AAPT2_LINK_H
 #define AAPT2_LINK_H
 
+#include <optional>
 #include <regex>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
 
 #include "Command.h"
 #include "Resource.h"
 #include "androidfw/IDiagnostics.h"
+#include "cmd/Util.h"
 #include "format/binary/TableFlattener.h"
 #include "format/proto/ProtoSerialize.h"
 #include "link/ManifestFixer.h"
@@ -72,6 +78,7 @@
   bool use_sparse_encoding = false;
   std::unordered_set<std::string> extensions_to_not_compress;
   std::optional<std::regex> regex_to_not_compress;
+  FeatureFlagValues feature_flag_values;
 
   // Static lib options.
   bool no_static_lib_packages = false;
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index a92f24b..678d846 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -113,6 +113,56 @@
   return std::move(filter);
 }
 
+bool ParseFeatureFlagsParameter(StringPiece arg, android::IDiagnostics* diag,
+                                FeatureFlagValues* out_feature_flag_values) {
+  if (arg.empty()) {
+    return true;
+  }
+
+  for (StringPiece flag_and_value : util::Tokenize(arg, ',')) {
+    std::vector<std::string> parts = util::Split(flag_and_value, '=');
+    if (parts.empty()) {
+      continue;
+    }
+
+    if (parts.size() > 2) {
+      diag->Error(android::DiagMessage()
+                  << "Invalid feature flag and optional value '" << flag_and_value
+                  << "'. Must be in the format 'flag_name[=true|false]");
+      return false;
+    }
+
+    StringPiece flag_name = util::TrimWhitespace(parts[0]);
+    if (flag_name.empty()) {
+      diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg);
+      return false;
+    }
+
+    std::optional<bool> flag_value = {};
+    if (parts.size() == 2) {
+      StringPiece str_flag_value = util::TrimWhitespace(parts[1]);
+      if (!str_flag_value.empty()) {
+        flag_value = ResourceUtils::ParseBool(parts[1]);
+        if (!flag_value.has_value()) {
+          diag->Error(android::DiagMessage() << "Invalid value for feature flag '" << flag_and_value
+                                             << "'. Value must be 'true' or 'false'");
+          return false;
+        }
+      }
+    }
+
+    if (auto [it, inserted] =
+            out_feature_flag_values->try_emplace(std::string(flag_name), flag_value);
+        !inserted) {
+      // We are allowing the same flag to appear multiple times, last value wins.
+      diag->Note(android::DiagMessage()
+                 << "Value for feature flag '" << flag_name << "' was given more than once");
+      it->second = flag_value;
+    }
+  }
+  return true;
+}
+
 // Adjust the SplitConstraints so that their SDK version is stripped if it
 // is less than or equal to the minSdk. Otherwise the resources that have had
 // their SDK version stripped due to minSdk won't ever match.
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 712c07b..9ece5dd 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -17,8 +17,13 @@
 #ifndef AAPT_SPLIT_UTIL_H
 #define AAPT_SPLIT_UTIL_H
 
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
 #include <regex>
 #include <set>
+#include <string>
 #include <unordered_set>
 
 #include "AppInfo.h"
@@ -32,6 +37,8 @@
 
 namespace aapt {
 
+using FeatureFlagValues = std::map<std::string, std::optional<bool>, std::less<>>;
+
 // Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc).
 // Returns Nothing and logs a human friendly error message if the string was not legal.
 std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg,
@@ -48,6 +55,13 @@
 std::unique_ptr<IConfigFilter> ParseConfigFilterParameters(const std::vector<std::string>& args,
                                                            android::IDiagnostics* diag);
 
+// Parses a feature flags parameter, which can contain one or more pairs of flag names and optional
+// values, and fills in `out_feature_flag_values` with the parsed values. The pairs in the argument
+// are separated by ',' and the name is separated from the value by '=' if there is a value given.
+// Example arg: "flag1=true,flag2=false,flag3=,flag4" where flag3 and flag4 have no given value.
+bool ParseFeatureFlagsParameter(android::StringPiece arg, android::IDiagnostics* diag,
+                                FeatureFlagValues* out_feature_flag_values);
+
 // Adjust the SplitConstraints so that their SDK version is stripped if it
 // is less than or equal to the min_sdk. Otherwise the resources that have had
 // their SDK version stripped due to min_sdk won't ever match.
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 139bfbc..723d87e 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -25,6 +25,7 @@
 #include "util/Files.h"
 
 using ::android::ConfigDescription;
+using testing::Pair;
 using testing::UnorderedElementsAre;
 
 namespace aapt {
@@ -354,6 +355,51 @@
   EXPECT_CONFIG_EQ(constraints, expected_configuration);
 }
 
+TEST(UtilTest, ParseFeatureFlagsParameter_Empty) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(ParseFeatureFlagsParameter("", diagnostics, &feature_flag_values));
+  EXPECT_TRUE(feature_flag_values.empty());
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_TooManyParts) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=bar=baz", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_NoNameGiven) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=true,=false", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_InvalidValue) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_FALSE(ParseFeatureFlagsParameter("foo=true,bar=42", diagnostics, &feature_flag_values));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(
+      ParseFeatureFlagsParameter("foo=true,bar=true,foo=false", diagnostics, &feature_flag_values));
+  EXPECT_THAT(feature_flag_values, UnorderedElementsAre(Pair("foo", std::optional<bool>(false)),
+                                                        Pair("bar", std::optional<bool>(true))));
+}
+
+TEST(UtilTest, ParseFeatureFlagsParameter_Valid) {
+  auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
+  FeatureFlagValues feature_flag_values;
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar =FALSE,baz=, quux", diagnostics,
+                                         &feature_flag_values));
+  EXPECT_THAT(feature_flag_values,
+              UnorderedElementsAre(Pair("foo", std::optional<bool>(true)),
+                                   Pair("bar", std::optional<bool>(false)),
+                                   Pair("baz", std::nullopt), Pair("quux", std::nullopt)));
+}
+
 TEST (UtilTest, AdjustSplitConstraintsForMinSdk) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
index 25abbac..c770b9c 100644
--- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
+++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
@@ -65,7 +65,7 @@
      */
     public static void onThrowMethodCalled() {
         // TODO: Maybe add call tracking?
-        throw new AssumptionViolatedException("This method is not supported on the host side");
+        throw new RuntimeException("This method is not supported on the host side");
     }
 
     /**
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index b133c2a..248121c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -377,7 +377,7 @@
                         throw HostStubGenInternalException("Policy $policy shouldn't show up here")
                     }
 
-                    val suffix = getAnnotationField(an, "suffix") ?: return@let
+                    val suffix = getAnnotationField(an, "suffix", false) ?: "\$ravenwood"
                     val renameFrom = mn.name + suffix
                     val renameTo = mn.name
 
@@ -387,13 +387,17 @@
                     }
 
                     // This mn has "SubstituteWith". This means,
-                        // 1. Re move the "rename-to" method, so add it to substitutedMethods.
+                    // 1. Re move the "rename-to" method, so add it to substitutedMethods.
                     policiesFromSubstitution[MethodKey(renameTo, mn.desc)] =
                             FilterPolicy.Remove.withReason("substitute-to")
 
+                    // If the policy is "stub", use "stub".
+                    // Otherwise, it must be "keep" or "throw", but there's no point in using
+                    // "throw", so let's use "keep".
+                    val newPolicy = if (policy.needsInStub) policy else FilterPolicy.Keep
                     // 2. We also keep the from-to in the map.
                     policiesFromSubstitution[MethodKey(renameFrom, mn.desc)] =
-                            policy.withReason("substitute-from")
+                            newPolicy.withReason("substitute-from")
                     substituteToMethods[MethodKey(renameFrom, mn.desc)] = renameTo
 
                     log.v("Substitution found: %s%s -> %s", renameFrom, mn.desc, renameTo)
@@ -405,10 +409,11 @@
     /**
      * Return the (String) value of 'value' parameter from an annotation.
      */
-    private fun getAnnotationField(an: AnnotationNode, name: String): String? {
+    private fun getAnnotationField(an: AnnotationNode, name: String,
+                                   required: Boolean = true): String? {
         try {
             val suffix = findAnnotationValueAsString(an, name)
-            if (suffix == null) {
+            if (suffix == null && required) {
                 errors.onErrorFound("Annotation \"${an.desc}\" must have field $name")
             }
             return suffix
@@ -438,4 +443,4 @@
             return ret
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tools/lint/README.md b/tools/lint/README.md
index b235ad6..ff8e442 100644
--- a/tools/lint/README.md
+++ b/tools/lint/README.md
@@ -103,10 +103,15 @@
 
 As noted above, this baseline file contains warnings too, which might be undesirable. For example,
 CI tools might surface these warnings in code reviews. In order to create this file without
-warnings, we need to pass another flag to lint: `--nowarn`. The easiest way to do this is to
-locally change the soong code in
-[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
-adding `cmd.Flag("--nowarn")` and running lint again.
+warnings, we need to pass another flag to lint: `--nowarn`. One option is to add the flag to your
+Android.bp file and then run lint again:
+
+```
+  lint: {
+    extra_check_modules: ["AndroidFrameworkLintChecker"],
+    flags: ["--nowarn"],
+  }
+```
 
 # Documentation
 
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
index d41fee3..24d203f 100644
--- a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -24,33 +24,31 @@
 import org.jetbrains.uast.UMethod
 
 /**
- * Given a UMethod, determine if this method is
- * the entrypoint to an interface generated by AIDL,
- * returning the interface name if so, otherwise returning null
+ * Given a UMethod, determine if this method is the entrypoint to an interface
+ * generated by AIDL, returning the interface name if so, otherwise returning
+ * null
  */
 fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
-    if (!isContainedInSubclassOfStub(context, node)) return null
-    for (superMethod in node.findSuperMethods()) {
-        for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
-            ?: continue) {
-            if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
-                return superMethod.containingClass?.name
-            }
-        }
+    val containingStub = containingStub(context, node) ?: return null
+    val superMethod = node.findSuperMethods(containingStub)
+    if (superMethod.isEmpty()) return null
+    return containingStub.containingClass?.name
+}
+
+/* Returns the containing Stub class if any. This is not sufficient to infer
+ * that the method itself extends an AIDL generated method. See
+ * getContainingAidlInterface for that purpose.
+ */
+fun containingStub(context: JavaContext, node: UMethod?): PsiClass? {
+    var superClass = node?.containingClass?.superClass
+    while (superClass != null) {
+        if (isStub(context, superClass)) return superClass
+        superClass = superClass.superClass
     }
     return null
 }
 
-fun isContainedInSubclassOfStub(context: JavaContext, node: UMethod?): Boolean {
-    var superClass = node?.containingClass?.superClass
-    while (superClass != null) {
-        if (isStub(context, superClass)) return true
-        superClass = superClass.superClass
-    }
-    return false
-}
-
-fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
+private fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
     if (psiClass == null) return false
     if (psiClass.name != "Stub") return false
     if (!context.evaluator.isStatic(psiClass)) return false
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 935bade..624a198 100644
--- a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -20,6 +20,7 @@
 import com.android.tools.lint.client.api.Vendor
 import com.android.tools.lint.detector.api.CURRENT_API
 import com.google.android.lint.parcel.SaferParcelChecker
+import com.google.android.lint.aidl.PermissionAnnotationDetector
 import com.google.auto.service.AutoService
 
 @AutoService(IssueRegistry::class)
@@ -37,6 +38,7 @@
         SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
         // TODO: Currently crashes due to OOM issue
         // PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+        PermissionAnnotationDetector.ISSUE_MISSING_PERMISSION_ANNOTATION,
         PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
         PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
     )
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionAnnotationDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionAnnotationDetector.kt
new file mode 100644
index 0000000..6b50cfd
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionAnnotationDetector.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Ensures all AIDL-generated methods are annotated.
+ *
+ * This detector is run on system_server to validate that any method that may
+ * be exposed via an AIDL interface is permission-annotated. That is, it must
+ * have one of the following annotation:
+ *   - @EnforcePermission
+ *   - @RequiresNoPermission
+ *   - @PermissionManuallyEnforced
+ */
+class PermissionAnnotationDetector : AidlImplementationDetector() {
+
+    override fun visitAidlMethod(
+      context: JavaContext,
+      node: UMethod,
+      interfaceName: String,
+      body: UBlockExpression
+    ) {
+        if (context.evaluator.isAbstract(node)) return
+
+        if (AIDL_PERMISSION_ANNOTATIONS.any { node.hasAnnotation(it) }) return
+
+        context.report(
+            ISSUE_MISSING_PERMISSION_ANNOTATION,
+            node,
+            context.getLocation(node),
+            "The method ${node.name} is not permission-annotated."
+        )
+    }
+
+    companion object {
+
+        private val EXPLANATION_MISSING_PERMISSION_ANNOTATION = """
+          Interfaces that are exposed by system_server are required to have an annotation which
+          denotes the type of permission enforced. There are 3 possible options:
+            - @EnforcePermission
+            - @RequiresNoPermission
+            - @PermissionManuallyEnforced
+          See the documentation of each annotation for further details.
+
+          The annotation on the Java implementation must be the same that the AIDL interface
+          definition. This is verified by a lint in the build system.
+          """.trimIndent()
+
+        @JvmField
+        val ISSUE_MISSING_PERMISSION_ANNOTATION = Issue.create(
+            id = "MissingPermissionAnnotation",
+            briefDescription = "No permission annotation on exposed AIDL interface.",
+            explanation = EXPLANATION_MISSING_PERMISSION_ANNOTATION,
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                PermissionAnnotationDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            enabledByDefault = false
+        )
+    }
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PermissionAnnotationDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PermissionAnnotationDetectorTest.kt
new file mode 100644
index 0000000..bce848a
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PermissionAnnotationDetectorTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class PermissionAnnotationDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = PermissionAnnotationDetector()
+
+    override fun getIssues(): List<Issue> = listOf(
+        PermissionAnnotationDetector.ISSUE_MISSING_PERMISSION_ANNOTATION,
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    /** No issue scenario */
+
+    fun testDoesNotDetectIssuesInCorrectScenario() {
+        lint().files(
+            java(
+            """
+            public class Foo extends IFoo.Stub {
+                @Override
+                @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+                public void testMethod() { }
+            }
+            """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    fun testMissingAnnotation() {
+        lint().files(
+            java(
+            """
+            public class Bar extends IBar.Stub {
+                public void testMethod() { }
+            }
+            """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Bar.java:2: Error: The method testMethod is not permission-annotated. [MissingPermissionAnnotation]
+                    public void testMethod() { }
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testNoIssueWhenExtendingWithAnotherSubclass() {
+        lint().files(
+            java(
+            """
+            public class Foo extends IFoo.Stub {
+                @Override
+                @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+                public void testMethod() { }
+                // not an AIDL method, just another method
+                public void someRandomMethod() { }
+            }
+            """).indented(),
+            java(
+            """
+            public class Baz extends Bar {
+              @Override
+              public void someRandomMethod() { }
+            }
+            """).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    /* Stubs */
+
+    // A service with permission annotation on the method.
+    private val interfaceIFoo: TestFile = java(
+        """
+        public interface IFoo extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IFoo {
+          }
+          @Override
+          @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+          public void testMethod();
+          @Override
+          @android.annotation.RequiresNoPermission
+          public void testMethodNoPermission();
+          @Override
+          @android.annotation.PermissionManuallyEnforced
+          public void testMethodManual();
+        }
+        """
+    ).indented()
+
+    // A service with no permission annotation.
+    private val interfaceIBar: TestFile = java(
+        """
+        public interface IBar extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IBar {
+          }
+          public void testMethod();
+        }
+        """
+    ).indented()
+
+    private val stubs = arrayOf(interfaceIFoo, interfaceIBar)
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
index 83b8f16..4455a9c 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
@@ -168,7 +168,7 @@
             annotationInfo.origin == AnnotationOrigin.METHOD) {
             /* Ignore implementations that are not a sub-class of Stub (i.e., Proxy). */
             val uMethod = element as? UMethod ?: return
-            if (!isContainedInSubclassOfStub(context, uMethod)) {
+            if (getContainingAidlInterface(context, uMethod) == null) {
                 return
             }
             val overridingMethod = element.sourcePsi as PsiMethod
@@ -184,7 +184,8 @@
             if (context.evaluator.isAbstract(node)) return
             if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return
 
-            if (!isContainedInSubclassOfStub(context, node)) {
+            val stubClass = containingStub(context, node)
+            if (stubClass == null) {
                 context.report(
                     ISSUE_MISUSING_ENFORCE_PERMISSION,
                     node,
@@ -196,7 +197,7 @@
 
             /* Check that we are connected to the super class */
             val overridingMethod = node as PsiMethod
-            val parents = overridingMethod.findSuperMethods()
+            val parents = overridingMethod.findSuperMethods(stubClass)
             if (parents.isEmpty()) {
                 context.report(
                     ISSUE_MISUSING_ENFORCE_PERMISSION,
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
index d8afcb9..2afca05 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
@@ -176,6 +176,29 @@
                 """.addLineContinuation())
     }
 
+    fun testDetectNoIssuesAnnotationOnNonStubMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            public class TestClass43 extends IFooMethod.Stub {
+                public void aRegularMethodNotPartOfStub() {
+                }
+            }
+            """).indented(), java(
+            """
+            package test.pkg;
+            public class TestClass44 extends TestClass43 {
+              @Override
+              public void aRegularMethodNotPartOfStub() {
+              }
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expectClean()
+    }
+
     fun testDetectIssuesEmptyAnnotationOnMethod() {
         lint().files(java(
             """
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
index ebda6f1..4f5e0e4 100644
--- a/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
@@ -70,14 +70,8 @@
     private List<HotspotNetwork> mHotspotNetworks = Collections.emptyList();
     private List<KnownNetwork> mKnownNetworks = Collections.emptyList();
     private SharedConnectivitySettingsState mSettingsState = null;
-    private HotspotNetworkConnectionStatus mHotspotNetworkConnectionStatus =
-            new HotspotNetworkConnectionStatus.Builder()
-                    .setStatus(HotspotNetworkConnectionStatus.CONNECTION_STATUS_UNKNOWN)
-                    .setExtras(Bundle.EMPTY).build();
-    private KnownNetworkConnectionStatus mKnownNetworkConnectionStatus =
-            new KnownNetworkConnectionStatus.Builder()
-                    .setStatus(KnownNetworkConnectionStatus.CONNECTION_STATUS_UNKNOWN)
-                    .setExtras(Bundle.EMPTY).build();
+    private HotspotNetworkConnectionStatus mHotspotNetworkConnectionStatus = null;
+    private KnownNetworkConnectionStatus mKnownNetworkConnectionStatus = null;
     // Used for testing
     private CountDownLatch mCountDownLatch;
 
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
index c6f6798..48ac82d 100644
--- a/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
@@ -394,6 +394,26 @@
         verify(mCallback, never()).onKnownNetworkConnectionStatusChanged(any());
     }
 
+    @Test
+    public void getHotspotNetworkConnectionStatus_withoutUpdate_returnsNull()
+            throws RemoteException {
+        SharedConnectivityService service = createService();
+        ISharedConnectivityService.Stub binder =
+                (ISharedConnectivityService.Stub) service.onBind(new Intent());
+
+        assertThat(binder.getHotspotNetworkConnectionStatus()).isNull();
+    }
+
+    @Test
+    public void getKnownNetworkConnectionStatus_withoutUpdate_returnsNull()
+            throws RemoteException {
+        SharedConnectivityService service = createService();
+        ISharedConnectivityService.Stub binder =
+                (ISharedConnectivityService.Stub) service.onBind(new Intent());
+
+        assertThat(binder.getKnownNetworkConnectionStatus()).isNull();
+    }
+
     private FakeSharedConnectivityService createService() {
         FakeSharedConnectivityService service = new FakeSharedConnectivityService();
         service.attachBaseContext(mContext);