Merge "Integrate isHardwareIgnoringTouches into OperationContext" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index edd9d3a..ce311d0 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -73,6 +73,7 @@
     ":android.provider.flags-aconfig-java{.generated_srcjars}",
     ":android.chre.flags-aconfig-java{.generated_srcjars}",
     ":android.speech.flags-aconfig-java{.generated_srcjars}",
+    ":power_flags_lib{.generated_srcjars}",
 ]
 
 filegroup {
@@ -932,3 +933,10 @@
     aconfig_declarations: "android.speech.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Power
+java_aconfig_library {
+    name: "power_flags_lib",
+    aconfig_declarations: "power_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ecfd86c..d59775f 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -138,14 +138,15 @@
     }
   ],
   "postsubmit-ravenwood": [
-    {
-      "name": "CtsUtilTestCasesRavenwood",
-      "host": true,
-      "file_patterns": [
-        "*Ravenwood*",
-        "*ravenwood*"
-      ]
-    }
+    // TODO(ravenwood) promote it to presubmit
+    // TODO: Enable it once the infra knows how to run it.
+//    {
+//      "name": "CtsUtilTestCasesRavenwood",
+//      "file_patterns": [
+//        "*Ravenwood*",
+//        "*ravenwood*"
+//      ]
+//    }
   ],
   "postsubmit-managedprofile-stress": [
     {
diff --git a/core/api/current.txt b/core/api/current.txt
index 8a1a466..46c8f82 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9486,12 +9486,15 @@
     method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForPackage(@NonNull String, @Nullable android.os.UserHandle);
     method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForProfile(@Nullable android.os.UserHandle);
     method public static android.appwidget.AppWidgetManager getInstance(android.content.Context);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int);
     method public boolean isRequestPinAppWidgetSupported();
     method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int);
     method @Deprecated public void notifyAppWidgetViewDataChanged(int, int);
     method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews);
     method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
     method public boolean requestPinAppWidget(@NonNull android.content.ComponentName, @Nullable android.os.Bundle, @Nullable android.app.PendingIntent);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") public void setWidgetPreview(@NonNull android.content.ComponentName, int, @NonNull android.widget.RemoteViews);
     method public void updateAppWidget(int[], android.widget.RemoteViews);
     method public void updateAppWidget(int, android.widget.RemoteViews);
     method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews);
@@ -9565,6 +9568,7 @@
     field public int autoAdvanceViewId;
     field public android.content.ComponentName configure;
     field @IdRes public int descriptionRes;
+    field @FlaggedApi("android.appwidget.flags.generated_previews") public int generatedPreviewCategories;
     field public int icon;
     field public int initialKeyguardLayout;
     field public int initialLayout;
@@ -18676,6 +18680,7 @@
     method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject, @NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
     method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
     method @Nullable public int getAllowedAuthenticators();
+    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView();
     method @Nullable public CharSequence getDescription();
     method @Nullable public CharSequence getNegativeButtonText();
     method @Nullable public CharSequence getSubtitle();
@@ -18723,6 +18728,7 @@
     method @NonNull public android.hardware.biometrics.BiometricPrompt build();
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setAllowedAuthenticators(int);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setConfirmationRequired(boolean);
+    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence);
     method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
@@ -18746,6 +18752,43 @@
     method @Nullable public java.security.Signature getSignature();
   }
 
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem {
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+    ctor public PromptContentListItemBulletedText(@NonNull CharSequence);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemBulletedText> CREATOR;
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+    ctor public PromptContentListItemPlainText(@NonNull CharSequence);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemPlainText> CREATOR;
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView {
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
+    method public int describeContents();
+    method @Nullable public CharSequence getDescription();
+    method @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems();
+    method public static int getMaxEachItemCharacterNumber();
+    method public static int getMaxItemCount();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptVerticalListContentView> CREATOR;
+  }
+
+  public static final class PromptVerticalListContentView.Builder {
+    ctor public PromptVerticalListContentView.Builder();
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem);
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build();
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence);
+  }
+
 }
 
 package android.hardware.camera2 {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ebd0544..bd4ecf2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -17023,6 +17023,7 @@
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteManager {
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatelliteService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getAllSatellitePlmnsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 4cf9fca..6204edc 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -19,6 +19,7 @@
 import static android.appwidget.flags.Flags.remoteAdapterConversion;
 
 import android.annotation.BroadcastBehavior;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresFeature;
@@ -30,6 +31,7 @@
 import android.annotation.UserIdInt;
 import android.app.IServiceConnection;
 import android.app.PendingIntent;
+import android.appwidget.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -1415,6 +1417,89 @@
         }
     }
 
+    /**
+     * Set a preview for this widget. This preview will be used instead of the provider's {@link
+     * AppWidgetProviderInfo#previewLayout previewLayout} or {@link
+     * AppWidgetProviderInfo#previewImage previewImage} for previewing the widget in the widget
+     * picker and pin app widget flow.
+     *
+     * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+     *    BroadcastReceiver} provider for the AppWidget you intend to provide a preview for.
+     * @param widgetCategories The categories that this preview should be used for. This can be a
+     *    single category or combination of categories. If multiple categories are specified,
+     *    then this preview will be used for each of those categories. For example, if you
+     *    set a preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, the preview will
+     *    be used when picking widgets for the home screen and keyguard.
+     *
+     *    <p>Note: You should only use the widget categories that the provider supports, as defined
+     *    in {@link AppWidgetProviderInfo#widgetCategory}.
+     * @param preview This preview will be used for previewing the provider when picking widgets for
+     *    the selected categories.
+     *
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX
+     */
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public void setWidgetPreview(@NonNull ComponentName provider,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+            @NonNull RemoteViews preview) {
+        try {
+            mService.setWidgetPreview(provider, widgetCategories, preview);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the RemoteViews previews for this widget.
+     *
+     * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+     *    BroadcastReceiver} provider for the AppWidget you intend to get a preview for.
+     * @param profile The profile in which the provider resides. Passing null is equivalent
+     *        to querying for only the calling user.
+     * @param widgetCategory The widget category for which you want to display previews. This should
+     *    be a single category. If a combination of categories is provided, this function will
+     *    return a preview that matches at least one of the categories.
+     *
+     * @return The widget preview for the selected category, if available.
+     * @see AppWidgetProviderInfo#generatedPreviewCategories
+     */
+    @Nullable
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public RemoteViews getWidgetPreview(@NonNull ComponentName provider,
+            @Nullable UserHandle profile, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+        try {
+            if (profile == null) {
+                profile = mContext.getUser();
+            }
+            return mService.getWidgetPreview(mPackageName, provider, profile.getIdentifier(),
+                    widgetCategory);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove this provider's preview for the specified widget categories. If the provider does not
+     * have a preview for the specified widget category, this is a no-op.
+     *
+     * @param provider The AppWidgetProvider to remove previews for.
+     * @param widgetCategories The categories of the preview to remove. For example, removing the
+     *    preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD will remove the
+     *    previews for both categories.
+     */
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public void removeWidgetPreview(@NonNull ComponentName provider,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+        try {
+            mService.removeWidgetPreview(provider, widgetCategories);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
     @UiThread
     private static @NonNull Executor createUpdateExecutorIfNull() {
         if (sUpdateExecutor == null) {
diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java
index e56e53a..1a80cac2 100644
--- a/core/java/android/appwidget/AppWidgetProviderInfo.java
+++ b/core/java/android/appwidget/AppWidgetProviderInfo.java
@@ -16,6 +16,10 @@
 
 package android.appwidget;
 
+import static android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS;
+import static android.appwidget.flags.Flags.generatedPreviews;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IdRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -358,6 +362,20 @@
     /** @hide */
     public boolean isExtendedFromAppWidgetProvider;
 
+    /**
+     * Flags indicating the widget categories for which generated previews are available.
+     * These correspond to the previews set by this provider with
+     * {@link AppWidgetManager#setWidgetPreview}.
+     *
+     * @see #WIDGET_CATEGORY_HOME_SCREEN
+     * @see #WIDGET_CATEGORY_KEYGUARD
+     * @see #WIDGET_CATEGORY_SEARCHBOX
+     * @see AppWidgetManager#getWidgetPreview
+     */
+    @FlaggedApi(FLAG_GENERATED_PREVIEWS)
+    @SuppressLint("MutableBareField")
+    public int generatedPreviewCategories;
+
     public AppWidgetProviderInfo() {
 
     }
@@ -391,6 +409,9 @@
         this.widgetFeatures = in.readInt();
         this.descriptionRes = in.readInt();
         this.isExtendedFromAppWidgetProvider = in.readBoolean();
+        if (generatedPreviews()) {
+            generatedPreviewCategories = in.readInt();
+        }
     }
 
     /**
@@ -515,6 +536,9 @@
         out.writeInt(this.widgetFeatures);
         out.writeInt(this.descriptionRes);
         out.writeBoolean(this.isExtendedFromAppWidgetProvider);
+        if (generatedPreviews()) {
+            out.writeInt(this.generatedPreviewCategories);
+        }
     }
 
     @Override
@@ -545,6 +569,9 @@
         that.widgetFeatures = this.widgetFeatures;
         that.descriptionRes = this.descriptionRes;
         that.isExtendedFromAppWidgetProvider = this.isExtendedFromAppWidgetProvider;
+        if (generatedPreviews()) {
+            that.generatedPreviewCategories = this.generatedPreviewCategories;
+        }
         return that;
     }
 
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 8c1ea5f..a0f4d8d 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -22,6 +22,7 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT;
 import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
@@ -208,6 +209,12 @@
 
         /**
          * Optional: Sets a description that will be shown on the prompt.
+         *
+         * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+         * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+         * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+         * called.
+         *
          * @param description The description to display.
          * @return This builder.
          */
@@ -218,6 +225,24 @@
         }
 
         /**
+         * Optional: Sets application customized content view that will be shown on the prompt.
+         *
+         * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+         * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+         * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+         * called.
+         *
+         * @param view The customized view information.
+         * @return This builder.re
+         */
+        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+        @NonNull
+        public BiometricPrompt.Builder setContentView(@NonNull PromptContentView view) {
+            mPromptInfo.setContentView(view);
+            return this;
+        }
+
+        /**
          * @param service
          * @return This builder.
          * @hide
@@ -698,6 +723,18 @@
     }
 
     /**
+     * Gets the content view for the prompt, as set by
+     * {@link Builder#setContentView(PromptContentView)}.
+     *
+     * @return The content view for the prompt, or null if the prompt has no content view.
+     */
+    @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+    @Nullable
+    public PromptContentView getContentView() {
+        return mPromptInfo.getContentView();
+    }
+
+    /**
      * Gets the negative button text for the prompt, as set by
      * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
      * @return The negative button text for the prompt, or null if no negative button text was set.
diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/hardware/biometrics/PromptContentListItem.java
new file mode 100644
index 0000000..fa3783d
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItem.java
@@ -0,0 +1,29 @@
+/*
+ * 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.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+
+/**
+ * A list item shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentListItem {
+}
+
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
new file mode 100644
index 0000000..c31f8a6
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
@@ -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 android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable {
+    private final CharSequence mText;
+
+    /**
+     * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentListItemBulletedText(@NonNull CharSequence text) {
+        mText = text;
+    }
+
+    /**
+     * @hide
+     */
+    @NonNull
+    public CharSequence getText() {
+        return mText;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeCharSequence(mText);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() {
+        @Override
+        public PromptContentListItemBulletedText createFromParcel(Parcel in) {
+            return new PromptContentListItemBulletedText(in.readCharSequence());
+        }
+
+        @Override
+        public PromptContentListItemBulletedText[] newArray(int size) {
+            return new PromptContentListItemBulletedText[size];
+        }
+    };
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
new file mode 100644
index 0000000..15271f0
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
@@ -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 android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
+
+/**
+ * A parcelable {@link PromptContentListItem}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable
+        permits PromptContentListItemPlainText, PromptContentListItemBulletedText {
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
new file mode 100644
index 0000000..d72a758
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
@@ -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 android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemPlainText implements PromptContentListItemParcelable {
+    private final CharSequence mText;
+
+    /**
+     * A list item with plain text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentListItemPlainText(@NonNull CharSequence text) {
+        mText = text;
+    }
+
+    /**
+     * @hide
+     */
+    @NonNull
+    public CharSequence getText() {
+        return mText;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeCharSequence(mText);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() {
+        @Override
+        public PromptContentListItemPlainText createFromParcel(Parcel in) {
+            return new PromptContentListItemPlainText(in.readCharSequence());
+        }
+
+        @Override
+        public PromptContentListItemPlainText[] newArray(int size) {
+            return new PromptContentListItemPlainText[size];
+        }
+    };
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentView.java b/core/java/android/hardware/biometrics/PromptContentView.java
new file mode 100644
index 0000000..ff9313e
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentView.java
@@ -0,0 +1,28 @@
+/*
+ * 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.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+
+/**
+ * Contains the information of the template of content view for Biometric Prompt.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentView {
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentViewParcelable.java b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
new file mode 100644
index 0000000..43b965b
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
@@ -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 android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
+
+/**
+ * A parcelable {@link PromptContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentViewParcelable extends PromptContentView, Parcelable
+        permits PromptVerticalListContentView {
+}
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 24cfd164..c73ebd4 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -35,6 +35,7 @@
     @Nullable private CharSequence mSubtitle;
     private boolean mUseDefaultSubtitle;
     @Nullable private CharSequence mDescription;
+    @Nullable private PromptContentViewParcelable mContentView;
     @Nullable private CharSequence mDeviceCredentialTitle;
     @Nullable private CharSequence mDeviceCredentialSubtitle;
     @Nullable private CharSequence mDeviceCredentialDescription;
@@ -60,6 +61,8 @@
         mSubtitle = in.readCharSequence();
         mUseDefaultSubtitle = in.readBoolean();
         mDescription = in.readCharSequence();
+        mContentView = in.readParcelable(PromptContentViewParcelable.class.getClassLoader(),
+                PromptContentViewParcelable.class);
         mDeviceCredentialTitle = in.readCharSequence();
         mDeviceCredentialSubtitle = in.readCharSequence();
         mDeviceCredentialDescription = in.readCharSequence();
@@ -100,6 +103,7 @@
         dest.writeCharSequence(mSubtitle);
         dest.writeBoolean(mUseDefaultSubtitle);
         dest.writeCharSequence(mDescription);
+        dest.writeParcelable(mContentView, 0);
         dest.writeCharSequence(mDeviceCredentialTitle);
         dest.writeCharSequence(mDeviceCredentialSubtitle);
         dest.writeCharSequence(mDeviceCredentialDescription);
@@ -176,6 +180,10 @@
         mDescription = description;
     }
 
+    public void setContentView(PromptContentView view) {
+        mContentView = (PromptContentViewParcelable) view;
+    }
+
     public void setDeviceCredentialTitle(CharSequence deviceCredentialTitle) {
         mDeviceCredentialTitle = deviceCredentialTitle;
     }
@@ -257,6 +265,15 @@
         return mDescription;
     }
 
+    /**
+     * Gets the content view for the prompt.
+     *
+     * @return The content view for the prompt, or null if the prompt has no content view.
+     */
+    public PromptContentView getContentView() {
+        return mContentView;
+    }
+
     public CharSequence getDeviceCredentialTitle() {
         return mDeviceCredentialTitle;
     }
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
new file mode 100644
index 0000000..f3cb189
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -0,0 +1,205 @@
+/*
+ * 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.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Contains the information of the template of vertical list content view for Biometric Prompt. Note
+ * that there are limits on the item count and the number of characters allowed for each item's
+ * text.
+ * <p>
+ * Here's how you'd set a <code>PromptVerticalListContentView</code> on a Biometric Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(...)
+ *     .setTitle(...)
+ *     .setSubTitle(...)
+ *     .setContentView(new PromptVerticalListContentView.Builder()
+ *         .setDescription("test description")
+ *         .addListItem(new PromptContentListItemPlainText("test item 1"))
+ *         .addListItem(new PromptContentListItemPlainText("test item 2"))
+ *         .addListItem(new PromptContentListItemBulletedText("test item 3"))
+ *         .build())
+ *     .build();
+ * </pre>
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptVerticalListContentView implements PromptContentViewParcelable {
+    private static final int MAX_ITEM_NUMBER = 20;
+    private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+    private final List<PromptContentListItemParcelable> mContentList;
+    private final CharSequence mDescription;
+
+    private PromptVerticalListContentView(
+            @NonNull List<PromptContentListItemParcelable> contentList,
+            @NonNull CharSequence description) {
+        mContentList = contentList;
+        mDescription = description;
+    }
+
+    private PromptVerticalListContentView(Parcel in) {
+        mContentList = in.readArrayList(
+                PromptContentListItemParcelable.class.getClassLoader(),
+                PromptContentListItemParcelable.class);
+        mDescription = in.readCharSequence();
+    }
+
+    /**
+     * Returns the maximum count of the list items.
+     */
+    public static int getMaxItemCount() {
+        return MAX_ITEM_NUMBER;
+    }
+
+    /**
+     * Returns the maximum number of characters allowed for each item's text.
+     */
+    public static int getMaxEachItemCharacterNumber() {
+        return MAX_EACH_ITEM_CHARACTER_NUMBER;
+    }
+
+    /**
+     * Gets the description for the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}.
+     *
+     * @return The description for the content view, or null if the content view has no description.
+     */
+    @Nullable
+    public CharSequence getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * Gets the list of ListItem on the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}.
+     *
+     * @return The item list on the content view.
+     */
+    @NonNull
+    public List<PromptContentListItem> getListItems() {
+        return new ArrayList<>(mContentList);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+        dest.writeList(mContentList);
+        dest.writeCharSequence(mDescription);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptVerticalListContentView> CREATOR = new Creator<>() {
+        @Override
+        public PromptVerticalListContentView createFromParcel(Parcel in) {
+            return new PromptVerticalListContentView(in);
+        }
+
+        @Override
+        public PromptVerticalListContentView[] newArray(int size) {
+            return new PromptVerticalListContentView[size];
+        }
+    };
+
+
+    /**
+     * A builder that collects arguments to be shown on the vertical list view.
+     */
+    public static final class Builder {
+        private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>();
+        private CharSequence mDescription;
+
+        /**
+         * Optional: Sets a description that will be shown on the content view.
+         *
+         * @param description The description to display.
+         * @return This builder.
+         */
+        @NonNull
+        public Builder setDescription(@NonNull CharSequence description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in
+         * total.
+         *
+         * @param listItem The list item view to display
+         * @return This builder.
+         */
+        @NonNull
+        public Builder addListItem(@NonNull PromptContentListItem listItem) {
+            if (doesListItemExceedsCharLimit(listItem)) {
+                throw new IllegalStateException(
+                        "The character number of list item exceeds "
+                                + MAX_EACH_ITEM_CHARACTER_NUMBER);
+            }
+            mContentList.add((PromptContentListItemParcelable) listItem);
+            return this;
+        }
+
+        private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) {
+            if (listItem instanceof PromptContentListItemPlainText) {
+                return ((PromptContentListItemPlainText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else if (listItem instanceof PromptContentListItemBulletedText) {
+                return ((PromptContentListItemBulletedText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else {
+                return false;
+            }
+        }
+
+
+        /**
+         * Creates a {@link PromptVerticalListContentView}.
+         *
+         * @return An instance of {@link PromptVerticalListContentView}.
+         */
+        @NonNull
+        public PromptVerticalListContentView build() {
+            if (mContentList.size() > MAX_ITEM_NUMBER) {
+                throw new IllegalStateException(
+                        "The number of list items exceeds " + MAX_ITEM_NUMBER);
+            }
+            return new PromptVerticalListContentView(mContentList, mDescription);
+        }
+    }
+}
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 375fdb5..3ba8be4 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -21,3 +21,10 @@
   bug: "307601768"
 }
 
+flag {
+  name: "custom_biometric_prompt"
+  namespace: "biometrics_framework"
+  description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
+  bug: "302735104"
+}
+
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 561db9c..83b7eda 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -80,8 +80,6 @@
      * <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater
      * than, or equal to this threshold.
      *
-     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
-     *
      * @hide
      */
     @NonNull
@@ -94,8 +92,6 @@
      * <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold,
      * the VCN will attempt to migrate away from the Carrier WiFi network.
      *
-     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
-     *
      * @hide
      */
     @NonNull
@@ -120,6 +116,15 @@
     public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
             "vcn_network_selection_ipsec_packet_loss_percent_threshold";
 
+    /**
+     * Key for the list of timeouts in minute to stop penalizing an underlying network candidate
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY =
+            "vcn_network_selection_penalty_timeout_minutes_list";
+
     // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
 
     /**
@@ -168,6 +173,7 @@
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
                 VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
                 VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
                 VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
                 VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 47065e1..f974ef4 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13476,6 +13476,16 @@
         @Readable
         public static final String OTA_DISABLE_AUTOMATIC_UPDATE = "ota_disable_automatic_update";
 
+
+        /**
+         * Whether to boot with 16K page size compatible kernel
+         * 1 = Boot with 16K kernel
+         * 0 = Boot with 4K kernel (default)
+         * @hide
+         */
+        @Readable
+        public static final String ENABLE_16K_PAGES = "enable_16k_pages";
+
         /** Timeout for package verification.
         * @hide */
         @Readable
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index bb7677d6..ccc5dbb 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -47,3 +47,10 @@
     is_fixed_read_only: true
 }
 
+flag {
+    name: "use_zero_jank_proxy"
+    namespace: "input_method"
+    description: "Feature flag for using a proxy that uses async calls to achieve zero jank for IMMS calls."
+    bug: "293640003"
+    is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index 4fe9aea..85bdbb9 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -80,5 +80,11 @@
             in Bundle extras, in IntentSender resultIntent);
     boolean isRequestPinAppWidgetSupported();
     oneway void noteAppWidgetTapped(in String callingPackage, in int appWidgetId);
+    void setWidgetPreview(in ComponentName providerComponent, in int widgetCategories,
+            in RemoteViews preview);
+    @nullable RemoteViews getWidgetPreview(in String callingPackage,
+            in ComponentName providerComponent, in int profileId, in int widgetCategory);
+    void removeWidgetPreview(in ComponentName providerComponent, in int widgetCategories);
+
 }
 
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index dcc9686..fbe1b8e 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -48,6 +48,7 @@
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
         <permission name="android.permission.READ_SEARCH_INDEXABLES"/>
         <permission name="android.permission.REBOOT"/>
+        <permission name="android.permission.RECOVERY"/>
         <permission name="android.permission.STATUS_BAR"/>
         <permission name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"/>
         <permission name="android.permission.TETHER_PRIVILEGED"/>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 896bcaf..e23e15f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1249,6 +1249,7 @@
         }
 
         String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
+        Log.v(TAG, "showOrHideAppBubble, with key: " + appBubbleKey);
         PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
         if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
 
@@ -1258,18 +1259,22 @@
             if (isStackExpanded()) {
                 if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
                     // App bubble is expanded, lets collapse
+                    Log.v(TAG, "  showOrHideAppBubble, selected bubble is app bubble, collapsing");
                     collapseStack();
                 } else {
                     // App bubble is not selected, select it
+                    Log.v(TAG, "  showOrHideAppBubble, expanded, selecting existing app bubble");
                     mBubbleData.setSelectedBubble(existingAppBubble);
                 }
             } else {
                 // App bubble is not selected, select it & expand
+                Log.v(TAG, "  showOrHideAppBubble, expand and select existing app bubble");
                 mBubbleData.setSelectedBubble(existingAppBubble);
                 mBubbleData.setExpanded(true);
             }
         } else {
             // App bubble does not exist, lets add and expand it
+            Log.v(TAG, "  showOrHideAppBubble, creating and expanding app bubble");
             Bubble b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
             b.setShouldAutoExpand(true);
             inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index d7cb490..4aed7c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.unfold;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
 import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
@@ -28,11 +30,11 @@
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 
+import dagger.Lazy;
+
 import java.util.List;
 import java.util.Optional;
 
-import dagger.Lazy;
-
 /**
  * Manages fold/unfold animations of tasks on foldable devices.
  * When folding or unfolding a foldable device we play animations that
@@ -228,7 +230,8 @@
     }
 
     private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
-        if (!mIsInStageChange) {
+        // TODO(b/311084698): the windowing mode check is added here as a work around.
+        if (!mIsInStageChange || taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
             // No need to resetTask if there is no ongoing state change.
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 41f8204..aabc1cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -421,9 +421,9 @@
             }
             moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
 
-            if (!mHasLongClicked) {
+            if (!mHasLongClicked && id != R.id.maximize_window) {
                 final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
-                decoration.closeMaximizeMenu();
+                decoration.closeMaximizeMenuIfNeeded(e);
             }
 
             final long eventDuration = e.getEventTime() - e.getDownTime();
@@ -643,7 +643,7 @@
                 handleCaptionThroughStatusBar(ev, relevantDecor);
             }
         }
-        handleEventOutsideFocusedCaption(ev, relevantDecor);
+        handleEventOutsideCaption(ev, relevantDecor);
         // Prevent status bar from reacting to a caption drag.
         if (DesktopModeStatus.isEnabled()) {
             if (mTransitionDragActive) {
@@ -652,11 +652,17 @@
         }
     }
 
-    // If an UP/CANCEL action is received outside of caption bounds, turn off handle menu
-    private void handleEventOutsideFocusedCaption(MotionEvent ev,
+    /**
+     * If an UP/CANCEL action is received outside of the caption bounds, close the handle and
+     * maximize the menu.
+     *
+     * @param relevantDecor the window decoration of the focused task's caption. This method only
+     *                      handles motion events outside this caption's bounds.
+     */
+    private void handleEventOutsideCaption(MotionEvent ev,
             DesktopModeWindowDecoration relevantDecor) {
         // Returns if event occurs within caption
-        if (relevantDecor == null || relevantDecor.checkTouchEventInCaptionHandle(ev)) {
+        if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) {
             return;
         }
 
@@ -692,7 +698,7 @@
                     }
 
                     if (dragFromStatusBarAllowed
-                            && relevantDecor.checkTouchEventInCaptionHandle(ev)) {
+                            && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
                         mTransitionDragActive = true;
                     }
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 5f77192..53f806c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -612,8 +612,7 @@
     void closeMaximizeMenuIfNeeded(MotionEvent ev) {
         if (!isMaximizeMenuActive()) return;
 
-        final PointF inputPoint = offsetCaptionLocation(ev);
-        if (!mMaximizeMenu.isValidMenuInput(inputPoint)) {
+        if (!mMaximizeMenu.isValidMenuInput(ev)) {
             closeMaximizeMenu();
         }
     }
@@ -639,20 +638,34 @@
     }
 
     /**
-     * Checks if motion event occurs in the caption handle area. This should be used in cases where
+     * Checks if motion event occurs in the caption handle area of a focused caption (the caption on
+     * a task in fullscreen or in multi-windowing mode). This should be used in cases where
      * onTouchListener will not work (i.e. when caption is in status bar area).
      *
      * @param ev       the {@link MotionEvent} to check
-     * @return {@code true} if event is inside the specified view, {@code false} if not
+     * @return {@code true} if event is inside caption handle view, {@code false} if not
      */
-    boolean checkTouchEventInCaptionHandle(MotionEvent ev) {
+    boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
         if (isHandleMenuActive() || !(mWindowDecorViewHolder
                 instanceof DesktopModeFocusedWindowDecorationViewHolder)) {
             return false;
         }
+
+        return checkTouchEventInCaption(ev);
+    }
+
+    /**
+     * Checks if touch event occurs in caption.
+     *
+     * @param ev       the {@link MotionEvent} to check
+     * @return {@code true} if event is inside caption view, {@code false} if not
+     */
+    boolean checkTouchEventInCaption(MotionEvent ev) {
         final PointF inputPoint = offsetCaptionLocation(ev);
-        return ((DesktopModeFocusedWindowDecorationViewHolder) mWindowDecorViewHolder)
-                .pointInCaption(inputPoint, mResult.mCaptionX);
+        return inputPoint.x >= mResult.mCaptionX
+                && inputPoint.x <= mResult.mCaptionX + mResult.mCaptionWidth
+                && inputPoint.y >= 0
+                && inputPoint.y <= mResult.mCaptionHeight;
     }
 
     /**
@@ -668,7 +681,7 @@
             // Click if point in caption handle view
             final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
             final View handle = caption.findViewById(R.id.caption_handle);
-            if (checkTouchEventInCaptionHandle(ev)) {
+            if (checkTouchEventInFocusedCaptionHandle(ev)) {
                 mOnCaptionButtonClickListener.onClick(handle);
             }
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 921708f..794b357 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -22,10 +22,10 @@
 import android.graphics.PixelFormat
 import android.graphics.PointF
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
 import android.view.SurfaceControlViewHost
-import android.view.View
 import android.view.View.OnClickListener
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
@@ -62,6 +62,8 @@
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
+    private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
+    private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
 
     /** Position the menu relative to the caption's position. */
     fun positionMenu(position: PointF, t: Transaction) {
@@ -95,8 +97,6 @@
                 .setName("Maximize Menu")
                 .setContainerLayer()
                 .build()
-        val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
-        val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
         val lp = WindowManager.LayoutParams(
                 menuWidth,
                 menuHeight,
@@ -160,14 +160,11 @@
      *
      * @param inputPoint the input to compare against.
      */
-    fun isValidMenuInput(inputPoint: PointF): Boolean {
-        val menuView = maximizeMenu?.mWindowViewHost?.view ?: return true
-        return !viewsLaidOut() || pointInView(menuView, inputPoint.x - menuPosition.x,
-                inputPoint.y - menuPosition.y)
-    }
-
-    private fun pointInView(v: View, x: Float, y: Float): Boolean {
-        return v.left <= x && v.right >= x && v.top <= y && v.bottom >= y
+    fun isValidMenuInput(ev: MotionEvent): Boolean {
+        val x = ev.rawX
+        val y = ev.rawY
+        return !viewsLaidOut() || (menuPosition.x <= x && menuPosition.x + menuWidth >= x &&
+                menuPosition.y <= y && menuPosition.y + menuHeight >= y)
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 6a9258c..afe837e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -279,11 +279,12 @@
         }
 
         outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
-        final int captionWidth = params.mCaptionWidthId != Resources.ID_NULL
+        outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
                 ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
-        outResult.mCaptionX = (outResult.mWidth - captionWidth) / 2;
+        outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
 
-        startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight)
+        startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth,
+                        outResult.mCaptionHeight)
                 .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */)
                 .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
                 .show(mCaptionContainerSurface);
@@ -356,7 +357,7 @@
         // Caption view
         mCaptionWindowManager.setConfiguration(taskConfig);
         final WindowManager.LayoutParams lp =
-                new WindowManager.LayoutParams(captionWidth, outResult.mCaptionHeight,
+                new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight,
                         WindowManager.LayoutParams.TYPE_APPLICATION,
                         WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
         lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
@@ -578,6 +579,7 @@
 
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
         int mCaptionHeight;
+        int mCaptionWidth;
         int mCaptionX;
         int mWidth;
         int mHeight;
@@ -587,6 +589,7 @@
             mWidth = 0;
             mHeight = 0;
             mCaptionHeight = 0;
+            mCaptionWidth = 0;
             mCaptionX = 0;
             mRootView = null;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
index 5f77022..6dcae27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -5,7 +5,6 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.res.ColorStateList
 import android.graphics.Color
-import android.graphics.PointF
 import android.view.View
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
 import android.widget.ImageButton
@@ -47,17 +46,6 @@
         animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
     }
 
-    /**
-     * Returns true if input point is in the caption's view.
-     * @param inputPoint the input point relative to the task in full "focus" (i.e. fullscreen).
-     */
-    fun pointInCaption(inputPoint: PointF, captionX: Int): Boolean {
-        return inputPoint.x >= captionX &&
-                inputPoint.x <= captionX + captionView.width &&
-                inputPoint.y >= 0 &&
-                inputPoint.y <= captionView.height
-    }
-
     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
         return if (shouldUseLightCaptionColors(taskInfo)) {
             context.getColor(R.color.desktop_mode_caption_handle_bar_light)
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 991fe41..c292b502 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -7,19 +7,14 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-android_app {
-    name: "CredentialManager",
-    defaults: ["platform_app_defaults"],
-    certificate: "platform",
+android_library {
+    name: "CredentialManager-handheld",
+
+    platform_apis: true,
+
     srcs: ["src/**/*.kt"],
     resource_dirs: ["res"],
 
-    dex_preopt: {
-        profile_guided: true,
-        //TODO: b/312357299 - Update baseline profile
-        profile: "profile.txt.prof",
-    },
-
     static_libs: [
         "CredentialManagerShared",
         "PlatformComposeCore",
@@ -42,6 +37,23 @@
         "androidx.recyclerview_recyclerview",
         "kotlinx-coroutines-core",
     ],
+}
+
+android_app {
+    name: "CredentialManager",
+    defaults: ["platform_app_defaults"],
+    certificate: "platform",
+
+    dex_preopt: {
+        profile_guided: true,
+        //TODO: b/312357299 - Update baseline profile
+        profile: "profile.txt.prof",
+    },
+
+    // Do not add new dependencies here. Add to CredentialManager-handheld instead.
+    static_libs: [
+        "CredentialManager-handheld",
+    ],
 
     platform_apis: true,
     privileged: true,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index c409ba6..f8ffc9e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -34,6 +34,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.res.stringResource
 import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.compose.theme.PlatformTheme
 import com.android.credentialmanager.common.Constants
 import com.android.credentialmanager.common.DialogState
 import com.android.credentialmanager.common.ProviderActivityResult
@@ -43,7 +44,6 @@
 import com.android.credentialmanager.createflow.hasContentToDisplay
 import com.android.credentialmanager.getflow.GetCredentialScreen
 import com.android.credentialmanager.getflow.hasContentToDisplay
-import com.android.credentialmanager.ui.theme.PlatformTheme
 
 @ExperimentalMaterialApi
 class CredentialSelectorActivity : ComponentActivity() {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
index db69b8b..d319e4c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
@@ -24,11 +24,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import com.android.compose.rememberSystemUiController
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.common.material.ModalBottomSheetLayout
 import com.android.credentialmanager.common.material.ModalBottomSheetValue
 import com.android.credentialmanager.common.material.rememberModalBottomSheetState
 import com.android.credentialmanager.ui.theme.EntryShape
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import kotlinx.coroutines.launch
 
 
@@ -54,7 +54,7 @@
         setBottomSheetSystemBarsColor(sysUiController)
     }
     ModalBottomSheetLayout(
-        sheetBackgroundColor = LocalAndroidColorScheme.current.colorSurfaceBright,
+        sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright,
         modifier = Modifier.background(Color.Transparent),
         sheetState = state,
         sheetContent = sheetContent,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
index 3976f9a..bdfe399 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
@@ -30,8 +30,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.ui.theme.Shapes
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 
 /**
  * Container card for the whole sheet.
@@ -50,7 +50,7 @@
         modifier = modifier.fillMaxWidth().wrapContentHeight(),
         border = null,
         colors = CardDefaults.cardColors(
-            containerColor = LocalAndroidColorScheme.current.colorSurfaceBright,
+            containerColor = LocalAndroidColorScheme.current.surfaceBright,
         ),
     ) {
         if (topAppBar != null) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 1c394ec..a6253b8 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -56,9 +56,9 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.R
 import com.android.credentialmanager.ui.theme.EntryShape
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.ui.theme.Shapes
 
 @Composable
@@ -168,7 +168,7 @@
                             // Decorative purpose only.
                             contentDescription = null,
                             modifier = Modifier.size(24.dp),
-                            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         )
                     }
                 }
@@ -182,7 +182,7 @@
                         Icon(
                             modifier = iconSize,
                             bitmap = iconImageBitmap,
-                            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                             // Decorative purpose only.
                             contentDescription = null,
                         )
@@ -206,7 +206,7 @@
                     Icon(
                         modifier = iconSize,
                         imageVector = iconImageVector,
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         // Decorative purpose only.
                         contentDescription = null,
                     )
@@ -218,7 +218,7 @@
                     Icon(
                         modifier = iconSize,
                         painter = iconPainter,
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         // Decorative purpose only.
                         contentDescription = null,
                     )
@@ -229,9 +229,9 @@
         },
         border = null,
         colors = SuggestionChipDefaults.suggestionChipColors(
-            containerColor = LocalAndroidColorScheme.current.colorSurfaceContainerHigh,
-            labelColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
-            iconContentColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+            containerColor = LocalAndroidColorScheme.current.surfaceContainerHigh,
+            labelColor = LocalAndroidColorScheme.current.onSurfaceVariant,
+            iconContentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
         ),
     )
 }
@@ -294,7 +294,7 @@
         Icon(
             modifier = Modifier.size(24.dp),
             painter = leadingIconPainter,
-            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
             // Decorative purpose only.
             contentDescription = null,
         )
@@ -353,7 +353,7 @@
                             R.string.accessibility_back_arrow_button
                         ),
                         modifier = Modifier.size(24.dp).autoMirrored(),
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                     )
                 }
             }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
index 2df0c7a9..342af3b 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
@@ -24,20 +24,20 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+import com.android.compose.theme.LocalAndroidColorScheme
 
 @Composable
 fun CredentialListSectionHeader(text: String, isFirstSection: Boolean) {
     InternalSectionHeader(
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         applyTopPadding = !isFirstSection
     )
 }
 
 @Composable
 fun MoreAboutPasskeySectionHeader(text: String) {
-    InternalSectionHeader(text, LocalAndroidColorScheme.current.colorOnSurface)
+    InternalSectionHeader(text, LocalAndroidColorScheme.current.onSurface)
 }
 
 @Composable
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
index a619523..b4075f1 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import com.android.compose.SystemUiController
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.common.material.ModalBottomSheetDefaults
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 
 @Composable
 fun setTransparentSystemBarsColor(sysUiController: SystemUiController) {
@@ -34,7 +34,7 @@
         darkIcons = false
     )
     sysUiController.setNavigationBarColor(
-        color = LocalAndroidColorScheme.current.colorSurfaceBright,
+        color = LocalAndroidColorScheme.current.surfaceBright,
         darkIcons = false
     )
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
index 6b46636..9111e61 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+import com.android.compose.theme.LocalAndroidColorScheme
 
 /**
  * The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X".
@@ -37,7 +37,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         textAlign = TextAlign.Center,
         style = MaterialTheme.typography.headlineSmall,
     )
@@ -51,7 +51,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.bodyMedium,
     )
 }
@@ -69,7 +69,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.bodySmall,
         overflow = TextOverflow.Ellipsis,
         maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -85,7 +85,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         style = MaterialTheme.typography.titleLarge,
     )
 }
@@ -103,7 +103,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         style = MaterialTheme.typography.titleSmall,
         overflow = TextOverflow.Ellipsis,
         maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -159,7 +159,7 @@
         modifier = modifier.wrapContentSize(),
         text = text,
         textAlign = TextAlign.Center,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.labelLarge,
     )
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 14a9165..f261d1f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -46,6 +46,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.CredentialSelectorViewModel
 import com.android.credentialmanager.R
 import com.android.credentialmanager.model.EntryInfo
@@ -70,7 +71,6 @@
 import com.android.credentialmanager.logging.CreateCredentialEvent
 import com.android.credentialmanager.model.creation.CreateOptionInfo
 import com.android.credentialmanager.model.creation.RemoteInfo
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import com.android.internal.logging.UiEventLogger.UiEventEnum
 
 @Composable
@@ -460,7 +460,7 @@
             item {
                 Divider(
                     thickness = 1.dp,
-                    color = LocalAndroidColorScheme.current.colorOutlineVariant,
+                    color = LocalAndroidColorScheme.current.outlineVariant,
                     modifier = Modifier.padding(vertical = 16.dp)
                 )
             }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index a291f59..458a99a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -26,7 +26,7 @@
 import com.android.internal.util.Preconditions
 
 data class GetCredentialUiState(
-        val isRequestForAllOptions: Boolean,
+    val isRequestForAllOptions: Boolean,
     val providerInfoList: List<ProviderInfo>,
     val requestDisplayInfo: RequestDisplayInfo,
     val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
@@ -165,7 +165,7 @@
     )
 }
 
-private fun toActiveEntry(
+fun toActiveEntry(
     providerDisplayInfo: ProviderDisplayInfo,
 ): EntryInfo? {
     val sortedUserNameToCredentialEntryList =
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
deleted file mode 100644
index a33904d..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import android.annotation.ColorInt
-import android.content.Context
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.graphics.Color
-import com.android.internal.R
-
-/** File copied from PlatformComposeCore. */
-
-/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
-val LocalAndroidColorScheme =
-    staticCompositionLocalOf<AndroidColorScheme> {
-        throw IllegalStateException(
-            "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " +
-                "Composable surrounded by a PlatformTheme {}."
-        )
-    }
-
-/**
- * The Android color scheme.
- *
- * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
- * most of the colors in this class will be removed in favor of their M3 counterpart.
- */
-class AndroidColorScheme internal constructor(context: Context) {
-    val colorSurfaceBright = getColor(context, R.attr.materialColorSurfaceBright)
-    val colorSurfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh)
-    val colorOutlineVariant = getColor(context, R.attr.materialColorOutlineVariant)
-    val colorOnSurface = getColor(context, R.attr.materialColorOnSurface)
-    val colorOnSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant)
-
-    companion object {
-        fun getColor(context: Context, attr: Int): Color {
-            val ta = context.obtainStyledAttributes(intArrayOf(attr))
-            @ColorInt val color = ta.getColor(0, 0)
-            ta.recycle()
-            return Color(color)
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt
deleted file mode 100644
index c923845..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import android.annotation.AttrRes
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-
-/** Read the [Color] from the given [attribute]. */
-@Composable
-@ReadOnlyComposable
-fun colorAttr(@AttrRes attribute: Int): Color {
-    return AndroidColorScheme.getColor(LocalContext.current, attribute)
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt
deleted file mode 100644
index 2f1ce68..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import com.android.credentialmanager.ui.theme.typography.TypeScaleTokens
-import com.android.credentialmanager.ui.theme.typography.TypefaceNames
-import com.android.credentialmanager.ui.theme.typography.TypefaceTokens
-import com.android.credentialmanager.ui.theme.typography.TypographyTokens
-import com.android.credentialmanager.ui.theme.typography.platformTypography
-
-/** File copied from PlatformComposeCore. */
-
-/**
- * The Material 3 theme that should wrap all Platform Composables.
- *
- * TODO(b/280685309): Merge with the official SysUI platform theme.
- */
-@Composable
-fun PlatformTheme(
-    isDarkTheme: Boolean = isSystemInDarkTheme(),
-    content: @Composable () -> Unit,
-) {
-    val context = LocalContext.current
-
-    val colorScheme =
-        if (isDarkTheme) {
-            dynamicDarkColorScheme(context)
-        } else {
-            dynamicLightColorScheme(context)
-        }
-    val androidColorScheme = AndroidColorScheme(context)
-    val typefaceNames = remember(context) { TypefaceNames.get(context) }
-    val typography =
-        remember(typefaceNames) {
-            platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
-        }
-
-    MaterialTheme(colorScheme, typography = typography) {
-        CompositionLocalProvider(
-            LocalAndroidColorScheme provides androidColorScheme,
-        ) {
-            content()
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt
deleted file mode 100644
index 984e4f1..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Typography
-
-/** File copied from PlatformComposeCore. */
-
-/**
- * The typography for Platform Compose code.
- *
- * Do not use directly and call [MaterialTheme.typography] instead to access the different text
- * styles.
- */
-internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
-    return Typography(
-        displayLarge = typographyTokens.displayLarge,
-        displayMedium = typographyTokens.displayMedium,
-        displaySmall = typographyTokens.displaySmall,
-        headlineLarge = typographyTokens.headlineLarge,
-        headlineMedium = typographyTokens.headlineMedium,
-        headlineSmall = typographyTokens.headlineSmall,
-        titleLarge = typographyTokens.titleLarge,
-        titleMedium = typographyTokens.titleMedium,
-        titleSmall = typographyTokens.titleSmall,
-        bodyLarge = typographyTokens.bodyLarge,
-        bodyMedium = typographyTokens.bodyMedium,
-        bodySmall = typographyTokens.bodySmall,
-        labelLarge = typographyTokens.labelLarge,
-        labelMedium = typographyTokens.labelMedium,
-        labelSmall = typographyTokens.labelSmall,
-    )
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt
deleted file mode 100644
index b2dd207..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.ui.unit.sp
-
-/** File copied from PlatformComposeCore. */
-internal class TypeScaleTokens(typefaceTokens: TypefaceTokens) {
-    val bodyLargeFont = typefaceTokens.plain
-    val bodyLargeLineHeight = 24.0.sp
-    val bodyLargeSize = 16.sp
-    val bodyLargeTracking = 0.0.sp
-    val bodyLargeWeight = TypefaceTokens.WeightRegular
-    val bodyMediumFont = typefaceTokens.plain
-    val bodyMediumLineHeight = 20.0.sp
-    val bodyMediumSize = 14.sp
-    val bodyMediumTracking = 0.0.sp
-    val bodyMediumWeight = TypefaceTokens.WeightRegular
-    val bodySmallFont = typefaceTokens.plain
-    val bodySmallLineHeight = 16.0.sp
-    val bodySmallSize = 12.sp
-    val bodySmallTracking = 0.1.sp
-    val bodySmallWeight = TypefaceTokens.WeightRegular
-    val displayLargeFont = typefaceTokens.brand
-    val displayLargeLineHeight = 64.0.sp
-    val displayLargeSize = 57.sp
-    val displayLargeTracking = 0.0.sp
-    val displayLargeWeight = TypefaceTokens.WeightRegular
-    val displayMediumFont = typefaceTokens.brand
-    val displayMediumLineHeight = 52.0.sp
-    val displayMediumSize = 45.sp
-    val displayMediumTracking = 0.0.sp
-    val displayMediumWeight = TypefaceTokens.WeightRegular
-    val displaySmallFont = typefaceTokens.brand
-    val displaySmallLineHeight = 44.0.sp
-    val displaySmallSize = 36.sp
-    val displaySmallTracking = 0.0.sp
-    val displaySmallWeight = TypefaceTokens.WeightRegular
-    val headlineLargeFont = typefaceTokens.brand
-    val headlineLargeLineHeight = 40.0.sp
-    val headlineLargeSize = 32.sp
-    val headlineLargeTracking = 0.0.sp
-    val headlineLargeWeight = TypefaceTokens.WeightRegular
-    val headlineMediumFont = typefaceTokens.brand
-    val headlineMediumLineHeight = 36.0.sp
-    val headlineMediumSize = 28.sp
-    val headlineMediumTracking = 0.0.sp
-    val headlineMediumWeight = TypefaceTokens.WeightRegular
-    val headlineSmallFont = typefaceTokens.brand
-    val headlineSmallLineHeight = 32.0.sp
-    val headlineSmallSize = 24.sp
-    val headlineSmallTracking = 0.0.sp
-    val headlineSmallWeight = TypefaceTokens.WeightRegular
-    val labelLargeFont = typefaceTokens.plain
-    val labelLargeLineHeight = 20.0.sp
-    val labelLargeSize = 14.sp
-    val labelLargeTracking = 0.0.sp
-    val labelLargeWeight = TypefaceTokens.WeightMedium
-    val labelMediumFont = typefaceTokens.plain
-    val labelMediumLineHeight = 16.0.sp
-    val labelMediumSize = 12.sp
-    val labelMediumTracking = 0.1.sp
-    val labelMediumWeight = TypefaceTokens.WeightMedium
-    val labelSmallFont = typefaceTokens.plain
-    val labelSmallLineHeight = 16.0.sp
-    val labelSmallSize = 11.sp
-    val labelSmallTracking = 0.1.sp
-    val labelSmallWeight = TypefaceTokens.WeightMedium
-    val titleLargeFont = typefaceTokens.brand
-    val titleLargeLineHeight = 28.0.sp
-    val titleLargeSize = 22.sp
-    val titleLargeTracking = 0.0.sp
-    val titleLargeWeight = TypefaceTokens.WeightRegular
-    val titleMediumFont = typefaceTokens.plain
-    val titleMediumLineHeight = 24.0.sp
-    val titleMediumSize = 16.sp
-    val titleMediumTracking = 0.0.sp
-    val titleMediumWeight = TypefaceTokens.WeightMedium
-    val titleSmallFont = typefaceTokens.plain
-    val titleSmallLineHeight = 20.0.sp
-    val titleSmallSize = 14.sp
-    val titleSmallTracking = 0.0.sp
-    val titleSmallWeight = TypefaceTokens.WeightMedium
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt
deleted file mode 100644
index 3cc761f..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalTextApi::class)
-
-package com.android.credentialmanager.ui.theme.typography
-
-import android.content.Context
-import androidx.compose.ui.text.ExperimentalTextApi
-import androidx.compose.ui.text.font.DeviceFontFamilyName
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-
-/** File copied from PlatformComposeCore. */
-internal class TypefaceTokens(typefaceNames: TypefaceNames) {
-    companion object {
-        val WeightMedium = FontWeight.Medium
-        val WeightRegular = FontWeight.Normal
-    }
-
-    private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
-    private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
-
-    val brand =
-        FontFamily(
-            Font(brandFont, weight = WeightMedium),
-            Font(brandFont, weight = WeightRegular),
-        )
-    val plain =
-        FontFamily(
-            Font(plainFont, weight = WeightMedium),
-            Font(plainFont, weight = WeightRegular),
-        )
-}
-
-internal data class TypefaceNames
-private constructor(
-    val brand: String,
-    val plain: String,
-) {
-    private enum class Config(val configName: String, val default: String) {
-        Brand("config_headlineFontFamily", "sans-serif"),
-        Plain("config_bodyFontFamily", "sans-serif"),
-    }
-
-    companion object {
-        fun get(context: Context): TypefaceNames {
-            return TypefaceNames(
-                brand = getTypefaceName(context, Config.Brand),
-                plain = getTypefaceName(context, Config.Plain),
-            )
-        }
-
-        private fun getTypefaceName(context: Context, config: Config): String {
-            return context
-                .getString(context.resources.getIdentifier(config.configName, "string", "android"))
-                .takeIf { it.isNotEmpty() }
-                ?: config.default
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt
deleted file mode 100644
index aadab92..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.ui.text.TextStyle
-
-/** File copied from PlatformComposeCore. */
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
-    val bodyLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodyLargeFont,
-            fontWeight = typeScaleTokens.bodyLargeWeight,
-            fontSize = typeScaleTokens.bodyLargeSize,
-            lineHeight = typeScaleTokens.bodyLargeLineHeight,
-            letterSpacing = typeScaleTokens.bodyLargeTracking,
-        )
-    val bodyMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodyMediumFont,
-            fontWeight = typeScaleTokens.bodyMediumWeight,
-            fontSize = typeScaleTokens.bodyMediumSize,
-            lineHeight = typeScaleTokens.bodyMediumLineHeight,
-            letterSpacing = typeScaleTokens.bodyMediumTracking,
-        )
-    val bodySmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodySmallFont,
-            fontWeight = typeScaleTokens.bodySmallWeight,
-            fontSize = typeScaleTokens.bodySmallSize,
-            lineHeight = typeScaleTokens.bodySmallLineHeight,
-            letterSpacing = typeScaleTokens.bodySmallTracking,
-        )
-    val displayLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.displayLargeFont,
-            fontWeight = typeScaleTokens.displayLargeWeight,
-            fontSize = typeScaleTokens.displayLargeSize,
-            lineHeight = typeScaleTokens.displayLargeLineHeight,
-            letterSpacing = typeScaleTokens.displayLargeTracking,
-        )
-    val displayMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.displayMediumFont,
-            fontWeight = typeScaleTokens.displayMediumWeight,
-            fontSize = typeScaleTokens.displayMediumSize,
-            lineHeight = typeScaleTokens.displayMediumLineHeight,
-            letterSpacing = typeScaleTokens.displayMediumTracking,
-        )
-    val displaySmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.displaySmallFont,
-            fontWeight = typeScaleTokens.displaySmallWeight,
-            fontSize = typeScaleTokens.displaySmallSize,
-            lineHeight = typeScaleTokens.displaySmallLineHeight,
-            letterSpacing = typeScaleTokens.displaySmallTracking,
-        )
-    val headlineLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineLargeFont,
-            fontWeight = typeScaleTokens.headlineLargeWeight,
-            fontSize = typeScaleTokens.headlineLargeSize,
-            lineHeight = typeScaleTokens.headlineLargeLineHeight,
-            letterSpacing = typeScaleTokens.headlineLargeTracking,
-        )
-    val headlineMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineMediumFont,
-            fontWeight = typeScaleTokens.headlineMediumWeight,
-            fontSize = typeScaleTokens.headlineMediumSize,
-            lineHeight = typeScaleTokens.headlineMediumLineHeight,
-            letterSpacing = typeScaleTokens.headlineMediumTracking,
-        )
-    val headlineSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineSmallFont,
-            fontWeight = typeScaleTokens.headlineSmallWeight,
-            fontSize = typeScaleTokens.headlineSmallSize,
-            lineHeight = typeScaleTokens.headlineSmallLineHeight,
-            letterSpacing = typeScaleTokens.headlineSmallTracking,
-        )
-    val labelLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelLargeFont,
-            fontWeight = typeScaleTokens.labelLargeWeight,
-            fontSize = typeScaleTokens.labelLargeSize,
-            lineHeight = typeScaleTokens.labelLargeLineHeight,
-            letterSpacing = typeScaleTokens.labelLargeTracking,
-        )
-    val labelMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelMediumFont,
-            fontWeight = typeScaleTokens.labelMediumWeight,
-            fontSize = typeScaleTokens.labelMediumSize,
-            lineHeight = typeScaleTokens.labelMediumLineHeight,
-            letterSpacing = typeScaleTokens.labelMediumTracking,
-        )
-    val labelSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelSmallFont,
-            fontWeight = typeScaleTokens.labelSmallWeight,
-            fontSize = typeScaleTokens.labelSmallSize,
-            lineHeight = typeScaleTokens.labelSmallLineHeight,
-            letterSpacing = typeScaleTokens.labelSmallTracking,
-        )
-    val titleLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleLargeFont,
-            fontWeight = typeScaleTokens.titleLargeWeight,
-            fontSize = typeScaleTokens.titleLargeSize,
-            lineHeight = typeScaleTokens.titleLargeLineHeight,
-            letterSpacing = typeScaleTokens.titleLargeTracking,
-        )
-    val titleMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleMediumFont,
-            fontWeight = typeScaleTokens.titleMediumWeight,
-            fontSize = typeScaleTokens.titleMediumSize,
-            lineHeight = typeScaleTokens.titleMediumLineHeight,
-            letterSpacing = typeScaleTokens.titleMediumTracking,
-        )
-    val titleSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleSmallFont,
-            fontWeight = typeScaleTokens.titleSmallWeight,
-            fontSize = typeScaleTokens.titleSmallSize,
-            lineHeight = typeScaleTokens.titleSmallLineHeight,
-            letterSpacing = typeScaleTokens.titleSmallTracking,
-        )
-}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index d26c5ff..16de478 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -231,6 +231,7 @@
                     Settings.Global.ENABLE_ADB_INCREMENTAL_INSTALL_DEFAULT,
                     Settings.Global.ENABLE_MULTI_SLOT_TIMEOUT_MILLIS,
                     Settings.Global.ENHANCED_4G_MODE_ENABLED,
+                    Settings.Global.ENABLE_16K_PAGES, // Added for 16K developer option
                     Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES,
                     Settings.Global.ERROR_LOGCAT_PREFIX,
                     Settings.Global.EUICC_PROVISIONED,
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 6c75b434..0df9bac 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -37,6 +37,7 @@
         "androidx.preference_preference",
         "androidx.viewpager_viewpager",
         "SettingsLibDisplayUtils",
+        "SettingsLibSettingsTheme",
         "com_android_a11y_menu_flags_lib",
     ],
 
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
index ca84265..648cc3b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -40,7 +40,7 @@
             android:exported="true"
             android:label="@string/accessibility_menu_settings_name"
             android:launchMode="singleTop"
-            android:theme="@style/AccessibilityMenuSettings">
+            android:theme="@style/Theme.SettingsBase">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
 
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
index eadcd7c..f5db6a4 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
@@ -8,10 +8,3 @@
     description: "Hides the AccessibilityMenuService UI before taking action instead of after."
     bug: "292020123"
 }
-
-flag {
-    name: "a11y_menu_settings_back_button_fix_and_large_button_sizing"
-    namespace: "accessibility"
-    description: "Provides/restores back button functionality for the a11yMenu settings page. Also, fixes sizing problems with large shortcut buttons."
-    bug: "298467628"
-}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
index 1f57654..81b3152 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
@@ -16,10 +16,6 @@
 -->
 
 <resources>
-  <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight">
-    <item name="android:windowLightStatusBar">false</item>
-  </style>
-
   <!--Adds the theme to support SnackBar component and user configurable theme. -->
   <style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight">
     <item name="android:colorControlNormal">@color/colorControlNormal</item>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
index a2508cd..4169155 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
@@ -16,11 +16,6 @@
 -->
 
 <resources>
-  <!--The theme is for preference CollapsingToolbarBaseActivity settings-->
-  <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight">
-    <item name="android:windowLightStatusBar">true</item>
-  </style>
-
   <!--Adds the theme to support SnackBar component and user configurable theme. -->
   <style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light">
     <item name="android:colorControlNormal">@color/colorControlNormal</item>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
index bf51e23..ab8f97a 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
@@ -35,7 +35,6 @@
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceManager;
 
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
 import com.android.systemui.accessibility.accessibilitymenu.R;
 
 /**
@@ -56,28 +55,17 @@
 
         ActionBar actionBar = getActionBar();
         actionBar.setDisplayShowCustomEnabled(true);
-
-        if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
-        }
+        actionBar.setDisplayHomeAsUpEnabled(true);
         actionBar.setCustomView(R.layout.preferences_action_bar);
         ((TextView) findViewById(R.id.action_bar_title)).setText(
                 getResources().getString(R.string.accessibility_menu_settings_name)
         );
-        actionBar.setDisplayOptions(
-                ActionBar.DISPLAY_TITLE_MULTIPLE_LINES
-                        | ActionBar.DISPLAY_SHOW_TITLE
-                        | ActionBar.DISPLAY_HOME_AS_UP);
     }
 
     @Override
     public boolean onNavigateUp() {
-        if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            mCallback.onBackInvoked();
-            return true;
-        } else {
-            return false;
-        }
+        mCallback.onBackInvoked();
+        return true;
     }
 
     /**
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
index c4f372c..c2cf6e1 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
@@ -28,7 +28,6 @@
 import android.widget.TextView;
 
 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
 import com.android.systemui.accessibility.accessibilitymenu.R;
 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
@@ -81,10 +80,8 @@
         if (convertView == null) {
             convertView = mInflater.inflate(R.layout.grid_item, parent, false);
 
-            if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-                configureShortcutSize(convertView,
-                        A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService));
-            }
+            configureShortcutSize(convertView,
+                    A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService));
         }
 
         A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position);
@@ -147,15 +144,6 @@
         ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
         TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
 
-        if (!Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            if (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)) {
-                ViewGroup.LayoutParams params = shortcutIconButton.getLayoutParams();
-                params.width = (int) (params.width * LARGE_BUTTON_SCALE);
-                params.height = (int) (params.height * LARGE_BUTTON_SCALE);
-                shortcutLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, mLargeTextSize);
-            }
-        }
-
         if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) {
             // Sets empty shortcut icon and label when the shortcut is ADD_ITEM.
             shortcutIconButton.setImageResource(android.R.color.transparent);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5fbffbe..c23a49c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -208,6 +208,13 @@
 }
 
 flag {
+    name: "compose_bouncer"
+    namespace: "systemui"
+    description: "Use the new compose bouncer in SystemUI"
+    bug: "310005730"
+}
+
+flag {
    name: "media_in_scene_container"
    namespace: "systemui"
    description: "Enable media in the scene container framework"
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index fd04b5ee0..f4ffb3c 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -22,6 +22,8 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -85,6 +87,12 @@
         throwComposeUnavailableError()
     }
 
+    override fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View = throwComposeUnavailableError()
+
     private fun throwComposeUnavailableError(): Nothing {
         error(
             "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index d31547b..43745f9 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -28,6 +28,9 @@
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.theme.PlatformTheme
 import com.android.compose.ui.platform.DensityAwareComposeView
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.composable.BouncerContent
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider
@@ -171,4 +174,14 @@
     private fun Int.toDp(context: Context): Dp {
         return (this.toFloat() / context.resources.displayMetrics.density).dp
     }
+
+    override fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View {
+        return ComposeView(context).apply {
+            setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
index 1860c9f..2b1268e 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
@@ -16,34 +16,14 @@
 
 package com.android.systemui.scene
 
-import android.app.AlertDialog
-import android.content.Context
-import com.android.systemui.bouncer.ui.composable.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.composable.BouncerScene
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.Scene
-import com.android.systemui.statusbar.phone.SystemUIDialog
 import dagger.Binds
 import dagger.Module
-import dagger.Provides
 import dagger.multibindings.IntoSet
 
 @Module
 interface BouncerSceneModule {
 
     @Binds @IntoSet fun bouncerScene(scene: BouncerScene): Scene
-
-    companion object {
-
-        @Provides
-        @SysUISingleton
-        fun bouncerSceneDialogFactory(@Application context: Context): BouncerDialogFactory {
-            return object : BouncerDialogFactory {
-                override fun invoke(): AlertDialog {
-                    return SystemUIDialog(context)
-                }
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 6591543..d949396 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -81,6 +81,7 @@
 import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
@@ -824,10 +825,6 @@
     }
 }
 
-interface BouncerDialogFactory {
-    operator fun invoke(): AlertDialog
-}
-
 /**
  * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of
  * the two reaches a stopping point but `0` in the middle of the transition.
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 d638ffe..428bc39 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
@@ -24,6 +24,7 @@
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.model.Direction
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 d76f0ff..91a4d2e 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
@@ -88,8 +88,6 @@
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.media.controls.ui.MediaHierarchyManager
-import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.res.R
 
 @Composable
@@ -576,10 +574,6 @@
     AndroidView(
         modifier = modifier,
         factory = {
-            viewModel.mediaHost.expansion = MediaHostState.EXPANDED
-            viewModel.mediaHost.showsOnlyActiveMedia = false
-            viewModel.mediaHost.falsingProtectionNeeded = false
-            viewModel.mediaHost.init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
             viewModel.mediaHost.hostView.layoutParams =
                 FrameLayout.LayoutParams(
                     FrameLayout.LayoutParams.MATCH_PARENT,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index 74f50d8..27d1eb7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
@@ -37,11 +37,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ColorCorrectionRepositoryImplTest : SysuiTestCase() {
-    companion object {
-        val TEST_USER_1 = UserHandle.of(1)!!
-        val TEST_USER_2 = UserHandle.of(2)!!
-    }
 
+    private val testUser1 = UserHandle.of(1)!!
+    private val testUser2 = UserHandle.of(2)!!
     private val testDispatcher = StandardTestDispatcher()
     private val scope = TestScope(testDispatcher)
     private val settings: FakeSettings = FakeSettings()
@@ -63,7 +61,7 @@
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 1,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
 
             underTest =
@@ -72,84 +70,84 @@
                     settings,
                 )
 
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             runCurrent()
 
-            val actualValue: Boolean = underTest.isEnabled(TEST_USER_1).first()
+            val actualValue: Boolean = underTest.isEnabled(testUser1).first()
             Truth.assertThat(actualValue).isTrue()
         }
 
     @Test
     fun isEnabled_settingUpdated_valueUpdated() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.ENABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
         }
 
     @Test
     fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
-            underTest.isEnabled(TEST_USER_2).launchIn(backgroundScope)
+            underTest.isEnabled(testUser2).launchIn(backgroundScope)
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_2.identifier
+                testUser2.identifier
             )
 
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.ENABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue()
+            Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse()
         }
 
     @Test
     fun setEnabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(true, TEST_USER_1)
+            val success = underTest.setIsEnabled(true, testUser1)
             runCurrent()
             Truth.assertThat(success).isTrue()
 
             val actualValue =
                 settings.getIntForUser(
                     ColorCorrectionRepositoryImpl.SETTING_NAME,
-                    TEST_USER_1.identifier
+                    testUser1.identifier
                 )
             Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.ENABLED)
         }
@@ -157,14 +155,14 @@
     @Test
     fun setDisabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(false, TEST_USER_1)
+            val success = underTest.setIsEnabled(false, testUser1)
             runCurrent()
             Truth.assertThat(success).isTrue()
 
             val actualValue =
                 settings.getIntForUser(
                     ColorCorrectionRepositoryImpl.SETTING_NAME,
-                    TEST_USER_1.identifier
+                    testUser1.identifier
                 )
             Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.DISABLED)
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3f05fef..423e124 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -39,6 +39,8 @@
 @RunWith(AndroidJUnit4::class)
 class ColorInversionRepositoryImplTest : SysuiTestCase() {
 
+    private val testUser1 = UserHandle.of(1)!!
+    private val testUser2 = UserHandle.of(2)!!
     private val testDispatcher = StandardTestDispatcher()
     private val scope = TestScope(testDispatcher)
     private val settings: FakeSettings = FakeSettings()
@@ -57,7 +59,7 @@
     @Test
     fun isEnabled_initiallyGetsSettingsValue() =
         scope.runTest {
-            settings.putIntForUser(SETTING_NAME, 1, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, 1, testUser1.identifier)
 
             underTest =
                 ColorInversionRepositoryImpl(
@@ -65,68 +67,68 @@
                     settings,
                 )
 
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             runCurrent()
 
-            val actualValue: Boolean = underTest.isEnabled(TEST_USER_1).first()
+            val actualValue: Boolean = underTest.isEnabled(testUser1).first()
             assertThat(actualValue).isTrue()
         }
 
     @Test
     fun isEnabled_settingUpdated_valueUpdated() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
 
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
 
-            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+            assertThat(underTest.isEnabled(testUser1).first()).isTrue()
 
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
         }
 
     @Test
     fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
-            underTest.isEnabled(TEST_USER_2).launchIn(backgroundScope)
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_2.identifier)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
+            underTest.isEnabled(testUser2).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier)
 
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
-            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser2).first()).isFalse()
 
-            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
-            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isTrue()
+            assertThat(underTest.isEnabled(testUser2).first()).isFalse()
         }
 
     @Test
     fun setEnabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(true, TEST_USER_1)
+            val success = underTest.setIsEnabled(true, testUser1)
             runCurrent()
             assertThat(success).isTrue()
 
-            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             assertThat(actualValue).isEqualTo(ENABLED)
         }
 
     @Test
     fun setDisabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(false, TEST_USER_1)
+            val success = underTest.setIsEnabled(false, testUser1)
             runCurrent()
             assertThat(success).isTrue()
 
-            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             assertThat(actualValue).isEqualTo(DISABLED)
         }
 
@@ -134,7 +136,5 @@
         private const val SETTING_NAME = ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
         private const val DISABLED = 0
         private const val ENABLED = 1
-        private val TEST_USER_1 = UserHandle.of(1)!!
-        private val TEST_USER_2 = UserHandle.of(2)!!
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 15633d1..4a39799 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.biometrics.BiometricManager.Authenticators
 import android.hardware.biometrics.ComponentInfoInternal
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
 import android.hardware.biometrics.SensorProperties
 import android.hardware.biometrics.SensorPropertiesInternal
@@ -119,6 +120,7 @@
     title: String = "title",
     subtitle: String = "sub",
     description: String = "desc",
+    contentView: PromptContentView? = null,
     credentialTitle: String? = "cred title",
     credentialSubtitle: String? = "cred sub",
     credentialDescription: String? = "cred desc",
@@ -128,6 +130,7 @@
     info.title = title
     info.subtitle = subtitle
     info.description = description
+    info.contentView = contentView
     credentialTitle?.let { info.deviceCredentialTitle = it }
     credentialSubtitle?.let { info.deviceCredentialSubtitle = it }
     credentialDescription?.let { info.deviceCredentialDescription = it }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
new file mode 100644
index 0000000..ccf119a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.SensorLocationInternal
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FingerprintPropertyInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fingerprintPropertyInteractor
+    private val repository = kosmos.fingerprintPropertyRepository
+    private val configurationRepository = kosmos.fakeConfigurationRepository
+    private val displayRepository = kosmos.displayRepository
+
+    @Test
+    fun sensorLocation_resolution1f() =
+        testScope.runTest {
+            val currSensorLocation by collectLastValue(underTest.sensorLocation)
+
+            displayRepository.emitDisplayChangeEvent(0)
+            runCurrent()
+            repository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations =
+                    mapOf(
+                        Pair("", SensorLocationInternal("", 4, 4, 2)),
+                        Pair("otherDisplay", SensorLocationInternal("", 1, 1, 1))
+                    )
+            )
+            runCurrent()
+            configurationRepository.setScaleForResolution(1f)
+            runCurrent()
+
+            assertThat(currSensorLocation?.centerX).isEqualTo(4)
+            assertThat(currSensorLocation?.centerY).isEqualTo(4)
+            assertThat(currSensorLocation?.radius).isEqualTo(2)
+        }
+
+    @Test
+    fun sensorLocation_resolution2f() =
+        testScope.runTest {
+            val currSensorLocation by collectLastValue(underTest.sensorLocation)
+
+            displayRepository.emitDisplayChangeEvent(0)
+            runCurrent()
+            repository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations =
+                    mapOf(
+                        Pair("", SensorLocationInternal("", 4, 4, 2)),
+                        Pair("otherDisplay", SensorLocationInternal("", 1, 1, 1))
+                    )
+            )
+            runCurrent()
+            configurationRepository.setScaleForResolution(2f)
+            runCurrent()
+
+            assertThat(currSensorLocation?.centerX).isEqualTo(4 * 2)
+            assertThat(currSensorLocation?.centerY).isEqualTo(4 * 2)
+            assertThat(currSensorLocation?.radius).isEqualTo(2 * 2)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index ee46f76..63f6c20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.TestableResources
 import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -60,7 +60,7 @@
 
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class PrimaryBouncerInteractorTest : SysuiTestCase() {
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private lateinit var repository: KeyguardBouncerRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 744b65f..cd83c07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -39,6 +40,9 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -69,9 +73,9 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        testScope = TestScope()
+        testScope = TestScope(StandardTestDispatcher())
 
-        val withDeps = CommunalInteractorFactory.create()
+        val withDeps = CommunalInteractorFactory.create(testScope)
 
         tutorialRepository = withDeps.tutorialRepository
         communalRepository = withDeps.communalRepository
@@ -379,6 +383,131 @@
         }
 
     @Test
+    fun transitionProgress_onTargetScene_fullProgress() =
+        testScope.runTest {
+            val targetScene = CommunalSceneKey.Blank
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(targetScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // We're on the target scene.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
+    fun transitionProgress_notOnTargetScene_noProgress() =
+        testScope.runTest {
+            val targetScene = CommunalSceneKey.Blank
+            val currentScene = CommunalSceneKey.Communal
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Transition progress is still idle, but we're not on the target scene.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+        }
+
+    @Test
+    fun transitionProgress_transitioningToTrackedScene() =
+        testScope.runTest {
+            val currentScene = CommunalSceneKey.Communal
+            val targetScene = CommunalSceneKey.Blank
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            var transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Progress starts at 0.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+
+            val progress = MutableStateFlow(0f)
+            transitionState =
+                MutableStateFlow(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Partially transition.
+            progress.value = .4f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(.4f))
+
+            // Transition is at full progress.
+            progress.value = 1f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(1f))
+
+            // Transition finishes.
+            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
+            underTest.setTransitionState(transitionState)
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
+    fun transitionProgress_transitioningAwayFromTrackedScene() =
+        testScope.runTest {
+            val currentScene = CommunalSceneKey.Blank
+            val targetScene = CommunalSceneKey.Communal
+            val transitionProgressFlow = underTest.transitionProgressToScene(currentScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            var transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Progress starts at 0.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+
+            val progress = MutableStateFlow(0f)
+            transitionState =
+                MutableStateFlow(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Partially transition.
+            progress.value = .4f
+
+            // This is a transition we don't care about the progress of.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+
+            // Transition is at full progress.
+            progress.value = 1f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+
+            // Transition finishes.
+            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
+            underTest.setTransitionState(transitionState)
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
     fun isCommunalShowing() =
         testScope.runTest {
             var isCommunalShowing = collectLastValue(underTest.isCommunalShowing)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index a776062..804c052 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.util.mockito.mock
@@ -48,6 +49,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -92,6 +94,13 @@
     }
 
     @Test
+    fun init_initsMediaHost() =
+        testScope.runTest {
+            // MediaHost is initialized as soon as the class is created.
+            verify(mediaHost).init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
+        }
+
+    @Test
     fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
         testScope.runTest {
             // Keyguard showing, and tutorial not started.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 5b88ebe6..fd2fd2f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -203,4 +203,38 @@
 
             assertThat(isVisible?.isAnimating).isEqualTo(false)
         }
+
+    @Test
+    fun alpha_glanceableHubOpen_isZero() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope,
+            )
+
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun alpha_glanceableHubClosed_isOne() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+
+            // Transition to the glanceable hub and back.
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope,
+            )
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+
+            assertThat(alpha).isEqualTo(1.0f)
+        }
 }
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
new file mode 100644
index 0000000..3908757
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/customized_view"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="vertical"
+    style="@style/AuthCredentialContentLayoutStyle">
+
+    <TextView
+        android:id="@+id/customized_view_title"
+        style="@style/TextAppearance.AuthCredential.ContentViewTitle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="marquee"
+        android:marqueeRepeatLimit="1"
+        android:singleLine="true" />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
new file mode 100644
index 0000000..e39f60f
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/TextAppearance.AuthCredential.ContentViewListItem"
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_weight="1.0" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
new file mode 100644
index 0000000..6c86736
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/biometric_prompt_content_list_row_height"
+    android:gravity="center_vertical|start"
+    android:orientation="horizontal" />
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index bea0e13..23fbb12 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -48,6 +48,23 @@
         android:importantForAccessibility="no"
         style="@style/TextAppearance.AuthCredential.Description"/>
 
+    <Space
+        android:id="@+id/space_above_content"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/biometric_prompt_space_above_content"
+        android:visibility="gone" />
+
+    <ScrollView
+        android:id="@+id/customized_view_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fadeScrollbars="false"
+        android:gravity="center_vertical"
+        android:orientation="vertical"
+        android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal"
+        android:scrollbars="vertical"
+        android:visibility="gone" />
+
     <Space android:id="@+id/space_above_icon"
         android:layout_width="match_parent"
         android:layout_height="48dp" />
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 5f6a39a..8be1cc7 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -135,6 +135,9 @@
     <color name="biometric_dialog_gray">#ff757575</color>
     <color name="biometric_dialog_accent">@color/material_dynamic_primary40</color>
     <color name="biometric_dialog_error">#ffd93025</color>                  <!-- red 600 -->
+    <!-- Color for biometric prompt content view -->
+    <color name="biometric_prompt_content_background_color">#8AB4F8</color>
+    <color name="biometric_prompt_content_list_item_bullet_color">#1d873b</color>
 
     <!-- SFPS colors -->
     <color name="sfps_chevron_fill">@color/material_dynamic_primary90</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 90d8cdb..798fc06b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1092,6 +1092,16 @@
     <dimen name="biometric_dialog_width">240dp</dimen>
     <dimen name="biometric_dialog_height">240dp</dimen>
 
+    <!-- Dimensions for biometric prompt content view. -->
+    <dimen name="biometric_prompt_space_above_content">48dp</dimen>
+    <dimen name="biometric_prompt_content_container_padding_horizontal">24dp</dimen>
+    <dimen name="biometric_prompt_content_padding_horizontal">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_row_height">24dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_padding_horizontal">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_text_size">14sp</dimen>
+    <dimen name="biometric_prompt_content_list_item_bullet_gap_width">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_bullet_radius">5dp</dimen>
+
     <!-- Biometric Auth Credential values -->
     <dimen name="biometric_auth_icon_size">48dp</dimen>
 
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index c925b26..fad4d4f 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -16,6 +16,7 @@
   -->
 <resources>
     <integer name="biometric_dialog_text_gravity">8388611</integer> <!-- gravity start -->
+    <integer name="biometric_prompt_content_list_item_max_lines_if_two_column">3</integer>
 
     <integer name="qs_security_footer_maxLines">2</integer>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7d7c050..ab3cacb 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -195,6 +195,24 @@
         <item name="android:textSize">14sp</item>
     </style>
 
+    <style name="TextAppearance.AuthCredential.ContentViewTitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingHorizontal">24dp</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:gravity">start</item>
+    </style>
+
+    <style name="TextAppearance.AuthCredential.ContentViewListItem">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingHorizontal">
+            @dimen/biometric_prompt_content_list_item_padding_horizontal
+        </item>
+        <item name="android:textSize">@dimen/biometric_prompt_content_list_item_text_size</item>
+        <item name="android:gravity">start</item>
+    </style>
+
     <style name="TextAppearance.AuthCredential.Error">
         <item name="android:paddingTop">6dp</item>
         <item name="android:paddingHorizontal">24dp</item>
@@ -294,6 +312,11 @@
         <item name="android:textSize">16sp</item>
     </style>
 
+    <style name="AuthCredentialContentLayoutStyle">
+        <item name="android:background">@color/biometric_prompt_content_background_color</item>
+        <item name="android:paddingHorizontal">@dimen/biometric_prompt_content_padding_horizontal</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index abbfa01..86802a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -131,23 +131,21 @@
                 icon.measure(
                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
-            } else if (child.getId() == R.id.space_above_icon) {
+            } else if (child.getId() == R.id.space_above_icon
+                    || child.getId() == R.id.space_above_content
+                    || child.getId() == R.id.button_bar) {
                 child.measure(
                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                         MeasureSpec.makeMeasureSpec(
                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
-            } else if (child.getId() == R.id.button_bar) {
-                child.measure(
-                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                        MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
-                                MeasureSpec.EXACTLY));
             } else if (child.getId() == R.id.space_below_icon) {
                 // Set the spacer height so the fingerprint icon is on the physical sensor area
                 final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
                 child.measure(
                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                         MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
-            } else if (child.getId() == R.id.description) {
+            } else if (child.getId() == R.id.description
+                    || child.getId() == R.id.customized_view_container) {
                 //skip description view and compute later
                 continue;
             } else {
@@ -161,27 +159,28 @@
             }
         }
 
-        //re-calculate the height of description
+        //re-calculate the height of body content
         View description = mView.findViewById(R.id.description);
+        View contentView = mView.findViewById(R.id.customized_view_container);
         if (description != null && description.getVisibility() != View.GONE) {
             totalHeight += measureDescription(description, displayHeight, width, totalHeight);
+        } else if (contentView != null && contentView.getVisibility() != View.GONE) {
+            totalHeight += measureDescription(contentView, displayHeight, width, totalHeight);
         }
 
         return new AuthDialog.LayoutParams(width, totalHeight);
     }
 
-    private int measureDescription(View description, int displayHeight, int currWidth,
+    private int measureDescription(View bodyContent, int displayHeight, int currWidth,
                                    int currHeight) {
-        //description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
-        //so we could getMeasuredHeight in onMeasureInternalPortrait directly.
-        int newHeight = description.getMeasuredHeight() + currHeight;
+        int newHeight = bodyContent.getMeasuredHeight() + currHeight;
         int limit = (int) (displayHeight * 0.75);
         if (newHeight > limit) {
-            description.measure(
+            bodyContent.measure(
                     MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
                     MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
         }
-        return description.getMeasuredHeight();
+        return bodyContent.getMeasuredHeight();
     }
 
     @NonNull
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
new file mode 100644
index 0000000..e6939f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.Context
+import android.hardware.biometrics.SensorLocationInternal
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorLocation
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+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
+
+@SysUISingleton
+class FingerprintPropertyInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    repository: FingerprintPropertyRepository,
+    configurationInteractor: ConfigurationInteractor,
+    displayStateInteractor: DisplayStateInteractor,
+) {
+    /**
+     * Devices with multiple physical displays use unique display ids to determine which sensor is
+     * on the active physical display. This value represents a unique physical display id.
+     */
+    private val uniqueDisplayId: Flow<String> =
+        displayStateInteractor.displayChanges
+            .map { context.display?.uniqueId }
+            .filterNotNull()
+            .distinctUntilChanged()
+
+    /**
+     * Sensor location for the:
+     * - current physical display
+     * - device's natural screen resolution
+     * - device's natural orientation
+     */
+    private val unscaledSensorLocation: Flow<SensorLocationInternal> =
+        combine(
+            repository.sensorLocations,
+            uniqueDisplayId,
+        ) { locations, displayId ->
+            // Devices without multiple physical displays do not use the display id as the key;
+            // instead, the key is an empty string.
+            locations.getOrDefault(
+                displayId,
+                locations.getOrDefault("", SensorLocationInternal.DEFAULT)
+            )
+        }
+
+    /**
+     * Sensor location for the:
+     * - current physical display
+     * - current screen resolution
+     * - device's natural orientation
+     */
+    val sensorLocation: Flow<SensorLocation> =
+        combine(
+            unscaledSensorLocation,
+            configurationInteractor.scaleForResolution,
+        ) { unscaledSensorLocation, scale ->
+            val sensorLocation =
+                SensorLocation(
+                    unscaledSensorLocation.sensorLocationX,
+                    unscaledSensorLocation.sensorLocationY,
+                    unscaledSensorLocation.sensorRadius,
+                )
+            sensorLocation.scale = scale
+            sensorLocation
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 8fbb250..4377937 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.biometrics.domain.model
 
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
 import com.android.systemui.biometrics.shared.model.BiometricModalities
 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
@@ -34,6 +35,7 @@
             operationInfo = operationInfo,
             showEmergencyCallButton = info.isShowEmergencyCallButton
         ) {
+        val contentView: PromptContentView? = info.contentView
         val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
new file mode 100644
index 0000000..dddadbd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.shared.model
+
+/** Provides current sensor location information in the current screen resolution [scale]. */
+data class SensorLocation(
+    private val naturalCenterX: Int,
+    private val naturalCenterY: Int,
+    private val naturalRadius: Int
+) {
+    /**
+     * Scale to apply to the sensor location's natural parameters to support different screen
+     * resolutions.
+     */
+    var scale: Float = 1f
+
+    val centerX: Float
+        get() {
+            return naturalCenterX * scale
+        }
+    val centerY: Float
+        get() {
+            return naturalCenterY * scale
+        }
+    val radius: Float
+        get() {
+            return naturalRadius * scale
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
index 0d72b9c..60b454e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -101,6 +101,7 @@
             final View child = getChildAt(i);
 
             if (child.getId() == R.id.space_above_icon
+                    || child.getId() == R.id.space_above_content
                     || child.getId() == R.id.space_below_icon
                     || child.getId() == R.id.button_bar) {
                 child.measure(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
new file mode 100644
index 0000000..22b02da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.binder
+
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import android.graphics.Paint
+import android.hardware.biometrics.PromptContentListItem
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentListItemPlainText
+import android.hardware.biometrics.PromptContentView
+import android.hardware.biometrics.PromptVerticalListContentView
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.BulletSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.Space
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import kotlin.math.ceil
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
+object BiometricCustomizedViewBinder {
+    fun bind(customizedViewContainer: ScrollView, spaceAbove: Space, viewModel: PromptViewModel) {
+        customizedViewContainer.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    val contentView: PromptContentView? = viewModel.contentView.first()
+
+                    if (contentView != null) {
+                        val context = customizedViewContainer.context
+                        customizedViewContainer.addView(contentView.toView(context))
+                        customizedViewContainer.visibility = View.VISIBLE
+                        spaceAbove.visibility = View.VISIBLE
+                    } else {
+                        customizedViewContainer.visibility = View.GONE
+                        spaceAbove.visibility = View.GONE
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun PromptContentView.toView(context: Context): View {
+    val resources = context.resources
+    val inflater = LayoutInflater.from(context)
+    when (this) {
+        is PromptVerticalListContentView -> {
+            val contentView =
+                inflater.inflate(R.layout.biometric_prompt_content_layout, null) as LinearLayout
+
+            val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_title)
+            if (!description.isNullOrEmpty()) {
+                descriptionView.text = description
+            } else {
+                descriptionView.visibility = View.GONE
+            }
+
+            // Show two column by default, once there is an item exceeding max lines, show single
+            // item instead.
+            val showTwoColumn = listItems.all { !it.doesExceedMaxLinesIfTwoColumn(resources) }
+            var currRowView = createNewRowLayout(inflater)
+            for (item in listItems) {
+                val itemView = item.toView(resources, inflater, context.theme)
+                currRowView.addView(itemView)
+
+                if (!showTwoColumn || currRowView.childCount == 2) {
+                    contentView.addView(currRowView)
+                    currRowView = createNewRowLayout(inflater)
+                }
+            }
+            if (currRowView.childCount > 0) {
+                contentView.addView(currRowView)
+            }
+
+            return contentView
+        }
+        else -> {
+            throw IllegalStateException("No such PromptContentView: $this")
+        }
+    }
+}
+
+private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout {
+    return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
+}
+
+private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn(
+    resources: Resources,
+): Boolean {
+    val passedInText: CharSequence =
+        when (this) {
+            is PromptContentListItemPlainText -> text
+            is PromptContentListItemBulletedText -> text
+            else -> {
+                throw IllegalStateException("No such ListItem: $this")
+            }
+        }
+
+    when (this) {
+        is PromptContentListItemPlainText,
+        is PromptContentListItemBulletedText -> {
+            val dialogMargin =
+                resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
+            val halfDialogWidth =
+                Resources.getSystem().displayMetrics.widthPixels / 2 - dialogMargin
+            val containerPadding =
+                resources.getDimensionPixelSize(
+                    R.dimen.biometric_prompt_content_container_padding_horizontal
+                )
+            val contentPadding =
+                resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal)
+            val listItemPadding = getListItemPadding(resources)
+            val maxWidth = halfDialogWidth - containerPadding - contentPadding - listItemPadding
+
+            val text = "$passedInText"
+            val textSize =
+                resources.getDimensionPixelSize(
+                    R.dimen.biometric_prompt_content_list_item_text_size
+                )
+            val paint = Paint()
+            paint.textSize = textSize.toFloat()
+
+            val maxLines =
+                resources.getInteger(
+                    R.integer.biometric_prompt_content_list_item_max_lines_if_two_column
+                )
+            val numLines = ceil(paint.measureText(text).toDouble() / maxWidth).toInt()
+            return numLines > maxLines
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+}
+
+private fun PromptContentListItem.toView(
+    resources: Resources,
+    inflater: LayoutInflater,
+    theme: Theme,
+): TextView {
+    val textView =
+        inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView
+    val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
+    textView.layoutParams = lp
+
+    when (this) {
+        is PromptContentListItemPlainText -> {
+            textView.text = text
+        }
+        is PromptContentListItemBulletedText -> {
+            val bulletedText = SpannableString(text)
+            val span =
+                BulletSpan(
+                    getListItemBulletGapWidth(resources),
+                    getListItemBulletColor(resources, theme),
+                    getListItemBulletRadius(resources)
+                )
+            bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            textView.text = bulletedText
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+    return textView
+}
+
+private fun PromptContentListItem.getListItemPadding(resources: Resources): Int {
+    var listItemPadding =
+        resources.getDimensionPixelSize(
+            R.dimen.biometric_prompt_content_list_item_padding_horizontal
+        ) * 2
+    when (this) {
+        is PromptContentListItemPlainText -> {}
+        is PromptContentListItemBulletedText -> {
+            listItemPadding +=
+                getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources)
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+    return listItemPadding
+}
+
+private fun getListItemBulletRadius(resources: Resources): Int =
+    resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_radius)
+
+private fun getListItemBulletGapWidth(resources: Resources): Int =
+    resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width)
+
+private fun getListItemBulletColor(resources: Resources, theme: Theme): Int =
+    resources.getColor(R.color.biometric_prompt_content_list_item_bullet_color, theme)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7b8cb82..04dc7a8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -31,6 +31,7 @@
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
 import android.view.accessibility.AccessibilityManager
 import android.widget.Button
+import android.widget.ScrollView
 import android.widget.TextView
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
@@ -94,6 +95,8 @@
         val titleView = view.requireViewById<TextView>(R.id.title)
         val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
         val descriptionView = view.requireViewById<TextView>(R.id.description)
+        val customizedViewContainer =
+            view.requireViewById<ScrollView>(R.id.customized_view_container)
 
         // set selected to enable marquee unless a screen reader is enabled
         titleView.isSelected =
@@ -150,8 +153,13 @@
             }
 
             titleView.text = viewModel.title.first()
-            descriptionView.text = viewModel.description.first()
             subtitleView.text = viewModel.subtitle.first()
+            descriptionView.text = viewModel.description.first()
+            BiometricCustomizedViewBinder.bind(
+                customizedViewContainer,
+                view.requireViewById(R.id.space_above_content),
+                viewModel
+            )
 
             // set button listeners
             negativeButton.setOnClickListener { legacyCallback.onButtonNegative() }
@@ -178,12 +186,14 @@
                             titleView,
                             subtitleView,
                             descriptionView,
+                            customizedViewContainer,
                         ),
                     viewsToFadeInOnSizeChange =
                         listOf(
                             titleView,
                             subtitleView,
                             descriptionView,
+                            customizedViewContainer,
                             indicatorMessageView,
                             negativeButton,
                             cancelButton,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 1a7b6c9..c3bbaed 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -55,7 +55,7 @@
     fun bind(
         view: BiometricPromptLayout,
         viewModel: PromptViewModel,
-        viewsToHideWhenSmall: List<TextView>,
+        viewsToHideWhenSmall: List<View>,
         viewsToFadeInOnSizeChange: List<View>,
         panelViewController: AuthPanelController,
         jankListener: BiometricJankListener,
@@ -110,7 +110,7 @@
 
                         // prepare for animated size transitions
                         for (v in viewsToHideWhenSmall) {
-                            v.showTextOrHide(forceHide = size.isSmall)
+                            v.showContentOrHide(forceHide = size.isSmall)
                         }
                         if (currentSize == null && size.isSmall) {
                             iconHolderView.alpha = 0f
@@ -119,6 +119,10 @@
                             viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
                         }
 
+                        // TODO(b/302735104): Fix wrong height due to the delay of
+                        // PromptContentView. addOnLayoutChangeListener() will cause crash when
+                        // showing credential view, since |PromptIconViewModel| won't release the
+                        // flow.
                         // propagate size changes to legacy panel controller and animate transitions
                         view.doOnLayout {
                             val width = view.measuredWidth
@@ -228,8 +232,9 @@
     return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
 }
 
-private fun TextView.showTextOrHide(forceHide: Boolean = false) {
-    visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+private fun View.showContentOrHide(forceHide: Boolean = false) {
+    val isTextViewWithBlankText = this is TextView && this.text.isBlank()
+    visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE
 }
 
 private fun View.asVerticalAnimator(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index d899827e..1c78928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.systemui.biometrics.ui.viewmodel
 
 import android.content.Context
 import android.graphics.Rect
 import android.hardware.biometrics.BiometricPrompt
+import android.hardware.biometrics.PromptContentView
 import android.util.Log
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
@@ -239,9 +241,20 @@
     val subtitle: Flow<String> =
         promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
 
-    /** Description for the prompt. */
-    val description: Flow<String> =
+    /** Custom content view for the prompt. */
+    val contentView: Flow<PromptContentView?> =
+        promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged()
+
+    private val originalDescription =
         promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+    /**
+     * Description for the prompt. Description view and contentView is mutually exclusive. Pass
+     * description down only when contentView is null.
+     */
+    val description: Flow<String> =
+        combine(contentView, originalDescription) { contentView, description ->
+            if (contentView == null) description else ""
+        }
 
     /** If the indicator (help, error) message should be shown. */
     val isIndicatorMessageVisible: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index c2a1d8f..d0ff185 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -90,6 +91,9 @@
     val alternateBouncerUIAvailable: StateFlow<Boolean>
     val sideFpsShowing: StateFlow<Boolean>
 
+    /** Action that should be run right after the bouncer is dismissed. */
+    var bouncerDismissActionModel: BouncerDismissActionModel?
+
     var lastAlternateBouncerVisibleTime: Long
 
     fun setPrimaryScrimmed(isScrimmed: Boolean)
@@ -134,6 +138,8 @@
     @Application private val applicationScope: CoroutineScope,
     @BouncerTableLog private val buffer: TableLogBuffer,
 ) : KeyguardBouncerRepository {
+    override var bouncerDismissActionModel: BouncerDismissActionModel? = null
+
     /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
     private val _primaryBouncerShow = MutableStateFlow(false)
     override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 654fa22..8c87b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -28,10 +28,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.DejankUtils
+import com.android.systemui.Flags
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
@@ -154,12 +156,12 @@
     /** Show the bouncer if necessary and set the relevant states. */
     @JvmOverloads
     fun show(isScrimmed: Boolean) {
-        if (primaryBouncerView.delegate == null) {
+        if (primaryBouncerView.delegate == null && !Flags.composeBouncer()) {
             Log.d(
                 TAG,
                 "PrimaryBouncerInteractor#show is being called before the " +
-                    "primaryBouncerDelegate is set. Let's exit early so we don't set the wrong " +
-                    "primaryBouncer state."
+                    "primaryBouncerDelegate is set. Let's exit early so we don't " +
+                    "set the wrong primaryBouncer state."
             )
             return
         }
@@ -272,15 +274,24 @@
         repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
     }
 
+    val bouncerDismissAction: BouncerDismissActionModel?
+        get() = repository.bouncerDismissActionModel
+
     /**
      * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
-     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
-     * call cancelAction.
+     * unlocked, we will run the onDismissAction. If the bouncer is exited before unlocking, we call
+     * cancelAction.
      */
     fun setDismissAction(
         onDismissAction: ActivityStarter.OnDismissAction?,
         cancelAction: Runnable?
     ) {
+        repository.bouncerDismissActionModel =
+            if (onDismissAction != null && cancelAction != null) {
+                BouncerDismissActionModel(onDismissAction, cancelAction)
+            } else {
+                null
+            }
         primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt
new file mode 100644
index 0000000..02b444f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.bouncer.shared.model
+
+import com.android.systemui.plugins.ActivityStarter
+
+/** Represents the action that needs to be performed after bouncer is dismissed. */
+data class BouncerDismissActionModel(
+    /** If the bouncer is unlocked, [onDismissAction] will be run. */
+    val onDismissAction: ActivityStarter.OnDismissAction?,
+    /** If the bouncer is exited before unlocking, [onCancel] will be invoked. */
+    val onCancel: Runnable?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt
new file mode 100644
index 0000000..5defe475
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.bouncer.ui
+
+import android.app.AlertDialog
+
+/** Factory to create alert dialogs for use in bouncer component. */
+interface BouncerDialogFactory {
+    operator fun invoke(): AlertDialog
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index 7f3b794..f3903de 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -16,9 +16,15 @@
 
 package com.android.systemui.bouncer.ui
 
+import android.app.AlertDialog
+import android.content.Context
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 @Module(
     includes =
@@ -29,4 +35,17 @@
 interface BouncerViewModule {
     /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
     @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
+
+    companion object {
+
+        @Provides
+        @SysUISingleton
+        fun bouncerDialogFactory(@Application context: Context): BouncerDialogFactory {
+            return object : BouncerDialogFactory {
+                override fun invoke(): AlertDialog {
+                    return SystemUIDialog(context)
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
new file mode 100644
index 0000000..dd253a8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -0,0 +1,89 @@
+package com.android.systemui.bouncer.ui.binder
+
+import android.view.ViewGroup
+import com.android.keyguard.KeyguardMessageAreaController
+import com.android.keyguard.ViewMediatorCallback
+import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.Flags
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags.COMPOSE_BOUNCER_ENABLED
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.log.BouncerLogger
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import dagger.Lazy
+import javax.inject.Inject
+
+/** Helper data class that allows to lazy load all the dependencies of the legacy bouncer. */
+@SysUISingleton
+data class LegacyBouncerDependencies
+@Inject
+constructor(
+    val viewModel: KeyguardBouncerViewModel,
+    val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+    val componentFactory: KeyguardBouncerComponent.Factory,
+    val messageAreaControllerFactory: KeyguardMessageAreaController.Factory,
+    val bouncerMessageInteractor: BouncerMessageInteractor,
+    val bouncerLogger: BouncerLogger,
+    val selectedUserInteractor: SelectedUserInteractor,
+)
+
+/** Helper data class that allows to lazy load all the dependencies of the compose based bouncer. */
+@SysUISingleton
+data class ComposeBouncerDependencies
+@Inject
+constructor(
+    val legacyInteractor: PrimaryBouncerInteractor,
+    val viewModel: BouncerViewModel,
+    val dialogFactory: BouncerDialogFactory,
+    val authenticationInteractor: AuthenticationInteractor,
+    val viewMediatorCallback: ViewMediatorCallback?,
+    val selectedUserInteractor: SelectedUserInteractor,
+)
+
+/**
+ * Toggles between the compose and non compose version of the bouncer, instantiating only the
+ * dependencies required for each.
+ */
+@SysUISingleton
+class BouncerViewBinder
+@Inject
+constructor(
+    private val legacyBouncerDependencies: Lazy<LegacyBouncerDependencies>,
+    private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>,
+) {
+    fun bind(view: ViewGroup) {
+        if (
+            ComposeFacade.isComposeAvailable() && Flags.composeBouncer() && COMPOSE_BOUNCER_ENABLED
+        ) {
+            val deps = composeBouncerDependencies.get()
+            ComposeBouncerViewBinder.bind(
+                view,
+                deps.legacyInteractor,
+                deps.viewModel,
+                deps.dialogFactory,
+                deps.authenticationInteractor,
+                deps.selectedUserInteractor,
+                deps.viewMediatorCallback,
+            )
+        } else {
+            val deps = legacyBouncerDependencies.get()
+            KeyguardBouncerViewBinder.bind(
+                view,
+                deps.viewModel,
+                deps.primaryBouncerToGoneTransitionViewModel,
+                deps.componentFactory,
+                deps.messageAreaControllerFactory,
+                deps.bouncerMessageInteractor,
+                deps.bouncerLogger,
+                deps.selectedUserInteractor,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
new file mode 100644
index 0000000..7b05395
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.bouncer.ui.binder
+
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.ViewMediatorCallback
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/** View binder responsible for binding the compose version of the bouncer. */
+object ComposeBouncerViewBinder {
+    fun bind(
+        view: ViewGroup,
+        legacyInteractor: PrimaryBouncerInteractor,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+        authenticationInteractor: AuthenticationInteractor,
+        selectedUserInteractor: SelectedUserInteractor,
+        viewMediatorCallback: ViewMediatorCallback?,
+    ) {
+        view.addView(
+            ComposeFacade.createBouncer(
+                view.context,
+                viewModel,
+                dialogFactory,
+            )
+        )
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    legacyInteractor.isShowing.collectLatest { bouncerShowing ->
+                        view.isVisible = bouncerShowing
+                    }
+                }
+
+                launch {
+                    authenticationInteractor.onAuthenticationResult.collectLatest {
+                        authenticationSucceeded ->
+                        if (authenticationSucceeded) {
+                            // Some dismiss actions require that keyguard be dismissed right away or
+                            // deferred until something else later on dismisses keyguard (eg. end of
+                            // a hide animation).
+                            val deferKeyguardDone =
+                                legacyInteractor.bouncerDismissAction?.onDismissAction?.onDismiss()
+                            legacyInteractor.setDismissAction(null, null)
+
+                            viewMediatorCallback?.let {
+                                val selectedUserId = selectedUserInteractor.getSelectedUserId()
+                                if (deferKeyguardDone == true) {
+                                    it.keyguardDonePending(selectedUserId)
+                                } else {
+                                    it.keyguardDone(selectedUserId)
+                                }
+                            }
+                        }
+                    }
+                }
+                launch {
+                    legacyInteractor.startingDisappearAnimation.collectLatest {
+                        it.run()
+                        legacyInteractor.hide()
+                    }
+                }
+            }
+        }
+    }
+}
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 24d4c6c..9fa4cd6 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
@@ -37,6 +37,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
@@ -77,6 +79,29 @@
         communalRepository.setTransitionState(transitionState)
     }
 
+    /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
+    fun transitionProgressToScene(targetScene: CommunalSceneKey) =
+        transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableCommunalTransitionState.Idle ->
+                        flowOf(CommunalTransitionProgress.Idle(state.scene))
+                    is ObservableCommunalTransitionState.Transition ->
+                        if (state.toScene == targetScene) {
+                            state.progress.map {
+                                CommunalTransitionProgress.Transition(
+                                    // Clamp the progress values between 0 and 1 as actual progress
+                                    // values can be higher than 0 or lower than 1 due to a fling.
+                                    progress = it.coerceIn(0.0f, 1.0f)
+                                )
+                            }
+                        } else {
+                            flowOf(CommunalTransitionProgress.OtherTransition)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+
     /**
      * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the
      * [CommunalSceneKey.Communal].
@@ -232,3 +257,17 @@
         }
     }
 }
+
+/** Simplified transition progress data class for tracking a single transition between scenes. */
+sealed class CommunalTransitionProgress {
+    /** No transition/animation is currently running. */
+    data class Idle(val scene: CommunalSceneKey) : CommunalTransitionProgress()
+
+    /** There is a transition animating to the expected scene. */
+    data class Transition(
+        val progress: Float,
+    ) : CommunalTransitionProgress()
+
+    /** There is a transition animating to a scene other than the expected scene. */
+    data object OtherTransition : CommunalTransitionProgress()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 7a96fab..09c18ed 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -23,7 +23,9 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.media.dagger.MediaModule
 import javax.inject.Inject
 import javax.inject.Named
@@ -70,6 +72,17 @@
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
         _isPopupOnDismissCtaShowing.asStateFlow()
 
+    init {
+        // Initialize our media host for the UMO. This only needs to happen once and must be done
+        // before the MediaHierarchyManager attempts to move the UMO to the hub.
+        with(mediaHost) {
+            expansion = MediaHostState.EXPANDED
+            showsOnlyActiveMedia = false
+            falsingProtectionNeeded = false
+            init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
+        }
+    }
+
     override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
 
     override fun onDismissCtaTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 3a92739..acbdecc 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -22,6 +22,8 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -88,6 +90,13 @@
         viewModel: BaseCommunalViewModel,
     ): View
 
+    /** Create a [View] to represent the [BouncerViewModel]. */
+    fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View
+
     /** Creates a container that hosts the communal UI and handles gesture transitions. */
     fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7876a6f..846736c 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -476,6 +476,13 @@
     // TODO(b/283300105): Tracking Bug
     @JvmField val SCENE_CONTAINER_ENABLED = false
 
+    /**
+     * Whether the compose bouncer is enabled. This ensures ProGuard can
+     * remove unused code from our APK at compile time.
+     */
+    // TODO(b/280877228): Tracking Bug
+    @JvmField val COMPOSE_BOUNCER_ENABLED = false
+
     // 1900
     @JvmField val NOTE_TASKS = releasedFlag("keycode_flag")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 19fd7f9..48b3d9a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -19,6 +19,7 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 import com.android.systemui.Flags
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -32,6 +33,7 @@
 class FromGlanceableHubTransitionInteractor
 @Inject
 constructor(
+    private val glanceableHubTransitions: GlanceableHubTransitions,
     override val transitionRepository: KeyguardTransitionRepository,
     transitionInteractor: KeyguardTransitionInteractor,
     @Main mainDispatcher: CoroutineDispatcher,
@@ -47,6 +49,7 @@
         if (!Flags.communalHub()) {
             return
         }
+        listenForHubToLockscreen()
     }
 
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -56,6 +59,18 @@
         }
     }
 
+    /**
+     * Listens for the glanceable hub transition to lock screen and directly drives the keyguard
+     * transition.
+     */
+    private fun listenForHubToLockscreen() {
+        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+            transitionName = "listenForHubToLockscreen",
+            transitionOwnerName = TAG,
+            toScene = CommunalSceneKey.Blank
+        )
+    }
+
     companion object {
         const val TAG = "FromGlanceableHubTransitionInteractor"
         val DEFAULT_DURATION = 500.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 2d0baa8..8b2b45f 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
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -62,6 +63,7 @@
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
     private val powerInteractor: PowerInteractor,
+    private val glanceableHubTransitions: GlanceableHubTransitions,
     inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
 ) :
     TransitionInteractor(
@@ -81,6 +83,7 @@
         listenForLockscreenToPrimaryBouncerDragging()
         listenForLockscreenToAlternateBouncer()
         listenForLockscreenTransitionToCamera()
+        listenForLockscreenToGlanceableHub()
     }
 
     /**
@@ -381,6 +384,22 @@
         }
     }
 
+    /**
+     * Listens for transition from glanceable hub back to lock screen and directly drives the
+     * keyguard transition.
+     */
+    private fun listenForLockscreenToGlanceableHub() {
+        if (!com.android.systemui.Flags.communalHub()) {
+            return
+        }
+
+        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+            transitionName = "listenForLockscreenToGlanceableHub",
+            transitionOwnerName = TAG,
+            toScene = CommunalSceneKey.Communal
+        )
+    }
+
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
@@ -406,5 +425,6 @@
         val TO_AOD_DURATION = 500.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_GONE_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
new file mode 100644
index 0000000..cb50839
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launch
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+class GlanceableHubTransitions
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    private val transitionRepository: KeyguardTransitionRepository,
+    private val communalInteractor: CommunalInteractor,
+) {
+    /**
+     * Listens for the glanceable hub transition to the specified scene and directly drives the
+     * keyguard transition between the lockscreen and the hub.
+     *
+     * The glanceable hub transition progress is used as the source of truth as it cannot be driven
+     * externally. The progress is used for both transitions caused by user touch input or by
+     * programmatic changes.
+     */
+    fun listenForLockscreenAndHubTransition(
+        transitionName: String,
+        transitionOwnerName: String,
+        toScene: CommunalSceneKey
+    ) {
+        val fromState: KeyguardState
+        val toState: KeyguardState
+        if (toScene == CommunalSceneKey.Blank) {
+            fromState = KeyguardState.GLANCEABLE_HUB
+            toState = KeyguardState.LOCKSCREEN
+        } else {
+            fromState = KeyguardState.LOCKSCREEN
+            toState = KeyguardState.GLANCEABLE_HUB
+        }
+        var transitionId: UUID? = null
+        scope.launch("$transitionOwnerName#$transitionName") {
+            communalInteractor
+                .transitionProgressToScene(toScene)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { pair ->
+                    val (transitionProgress, lastStartedStep) = pair
+
+                    val id = transitionId
+                    if (id == null) {
+                        // No transition started.
+                        if (
+                            transitionProgress is CommunalTransitionProgress.Transition &&
+                                lastStartedStep.to == fromState
+                        ) {
+                            transitionId =
+                                transitionRepository.startTransition(
+                                    TransitionInfo(
+                                        ownerName = transitionOwnerName,
+                                        from = fromState,
+                                        to = toState,
+                                        animator = null, // transition will be manually controlled
+                                    )
+                                )
+                        }
+                    } else {
+                        if (lastStartedStep.to != toState) {
+                            return@collect
+                        }
+                        // An existing `id` means a transition is started, and calls to
+                        // `updateTransition` will control it until FINISHED or CANCELED
+                        val nextState: TransitionState
+                        val progressFraction: Float
+                        when (transitionProgress) {
+                            is CommunalTransitionProgress.Idle -> {
+                                if (transitionProgress.scene == toScene) {
+                                    nextState = TransitionState.FINISHED
+                                    progressFraction = 1f
+                                } else {
+                                    nextState = TransitionState.CANCELED
+                                    progressFraction = 0f
+                                }
+                            }
+                            is CommunalTransitionProgress.Transition -> {
+                                nextState = TransitionState.RUNNING
+                                progressFraction = transitionProgress.progress
+                            }
+                            is CommunalTransitionProgress.OtherTransition -> {
+                                // Shouldn't happen but if another transition starts during the
+                                // current one, mark the current one as canceled.
+                                nextState = TransitionState.CANCELED
+                                progressFraction = 0f
+                            }
+                        }
+                        transitionRepository.updateTransition(
+                            id,
+                            progressFraction,
+                            nextState,
+                        )
+
+                        if (
+                            nextState == TransitionState.CANCELED ||
+                                nextState == TransitionState.FINISHED
+                        ) {
+                            transitionId = null
+                        }
+
+                        // If canceled, just put the state back.
+                        if (nextState == TransitionState.CANCELED) {
+                            transitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName = transitionOwnerName,
+                                    from = toState,
+                                    to = fromState,
+                                    animator =
+                                        ValueAnimator().apply {
+                                            interpolator = Interpolators.LINEAR
+                                            duration = 0
+                                        }
+                                )
+                            )
+                        }
+                    }
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 5b0c562..a8223ea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
@@ -135,10 +136,18 @@
     val lockscreenToDreamingLockscreenHostedTransition: Flow<TransitionStep> =
         repository.transition(LOCKSCREEN, DREAMING_LOCKSCREEN_HOSTED)
 
+    /** LOCKSCREEN->GLANCEABLE_HUB transition information. */
+    val lockscreenToGlanceableHubTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, GLANCEABLE_HUB)
+
     /** LOCKSCREEN->OCCLUDED transition information. */
     val lockscreenToOccludedTransition: Flow<TransitionStep> =
         repository.transition(LOCKSCREEN, OCCLUDED)
 
+    /** GLANCEABLE_HUB->LOCKSCREEN transition information. */
+    val glanceableHubToLockscreenTransition: Flow<TransitionStep> =
+        repository.transition(GLANCEABLE_HUB, LOCKSCREEN)
+
     /** OCCLUDED->LOCKSCREEN transition information. */
     val occludedToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(OCCLUDED, LOCKSCREEN)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index fa4de04..ce45112 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -17,14 +17,12 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.Context
-import android.hardware.biometrics.SensorLocationInternal
 import com.android.settingslib.Utils
-import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -44,21 +42,24 @@
     configurationInteractor: ConfigurationInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel,
-    fingerprintPropertyRepository: FingerprintPropertyRepository,
+    fingerprintPropertyInteractor: FingerprintPropertyInteractor,
     udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) {
     private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
+
+    /**
+     * UDFPS icon location in pixels for the current display and screen resolution, in natural
+     * orientation.
+     */
     val iconLocation: Flow<IconLocation> =
         isSupported.flatMapLatest { supportsUI ->
             if (supportsUI) {
-                fingerprintPropertyRepository.sensorLocations.map { sensorLocations ->
-                    val sensorLocation =
-                        sensorLocations.getOrDefault("", SensorLocationInternal.DEFAULT)
+                fingerprintPropertyInteractor.sensorLocation.map { sensorLocation ->
                     IconLocation(
-                        left = sensorLocation.sensorLocationX - sensorLocation.sensorRadius,
-                        top = sensorLocation.sensorLocationY - sensorLocation.sensorRadius,
-                        right = sensorLocation.sensorLocationX + sensorLocation.sensorRadius,
-                        bottom = sensorLocation.sensorLocationY + sensorLocation.sensorRadius,
+                        left = (sensorLocation.centerX - sensorLocation.radius).toInt(),
+                        top = (sensorLocation.centerY - sensorLocation.radius).toInt(),
+                        right = (sensorLocation.centerX + sensorLocation.radius).toInt(),
+                        bottom = (sensorLocation.centerY + sensorLocation.radius).toInt(),
                     )
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..bc51821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class GlanceableHubToLockscreenTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            from = KeyguardState.GLANCEABLE_HUB,
+            to = KeyguardState.LOCKSCREEN,
+        )
+
+    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
+    val keyguardAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            onStep = { it },
+            onFinish = { 1f },
+            onCancel = { 0f },
+            name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 5059e6b..5d36da9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -46,6 +46,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
@@ -58,6 +59,8 @@
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
+    glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
     screenOffAnimationController: ScreenOffAnimationController,
     private val aodBurnInViewModel: AodBurnInViewModel,
     aodAlphaViewModel: AodAlphaViewModel,
@@ -78,7 +81,13 @@
         keyguardInteractor.notificationContainerBounds
 
     /** An observable for the alpha level for the entire keyguard root view. */
-    val alpha: Flow<Float> = aodAlphaViewModel.alpha
+    val alpha: Flow<Float> =
+        merge(
+                aodAlphaViewModel.alpha,
+                lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
+                glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+            )
+            .distinctUntilChanged()
 
     /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
     val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
new file mode 100644
index 0000000..3ea83ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->GLANCEABLE_HUB transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class LockscreenToGlanceableHubTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GLANCEABLE_HUB,
+        )
+
+    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
+    val keyguardAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            onStep = { 1f - it },
+            onFinish = { 0f },
+            onCancel = { 1f },
+            name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index cde2a62..8c852cd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -31,16 +31,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
-import com.android.keyguard.KeyguardMessageAreaController;
 import com.android.keyguard.LockIconViewController;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
-import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
+import com.android.systemui.bouncer.ui.binder.BouncerViewBinder;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
@@ -56,8 +52,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies;
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
-import com.android.systemui.log.BouncerLogger;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -77,7 +71,6 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.SystemClock;
 
@@ -183,24 +176,18 @@
             DumpManager dumpManager,
             PulsingGestureListener pulsingGestureListener,
             LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener,
-            KeyguardBouncerViewModel keyguardBouncerViewModel,
-            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
-            KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
-            PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
             GlanceableHubContainerController glanceableHubContainerController,
             NotificationLaunchAnimationInteractor notificationLaunchAnimationInteractor,
             FeatureFlagsClassic featureFlagsClassic,
             SystemClock clock,
-            BouncerMessageInteractor bouncerMessageInteractor,
-            BouncerLogger bouncerLogger,
             SysUIKeyEventHandler sysUIKeyEventHandler,
             QuickSettingsController quickSettingsController,
             PrimaryBouncerInteractor primaryBouncerInteractor,
             AlternateBouncerInteractor alternateBouncerInteractor,
-            SelectedUserInteractor selectedUserInteractor,
             Lazy<JavaAdapter> javaAdapter,
-            Lazy<AlternateBouncerDependencies> alternateBouncerDependencies) {
+            Lazy<AlternateBouncerDependencies> alternateBouncerDependencies,
+            BouncerViewBinder bouncerViewBinder) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -234,15 +221,7 @@
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
         mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView);
-        KeyguardBouncerViewBinder.bind(
-                mView.findViewById(R.id.keyguard_bouncer_container),
-                keyguardBouncerViewModel,
-                primaryBouncerToGoneTransitionViewModel,
-                keyguardBouncerComponentFactory,
-                messageAreaControllerFactory,
-                bouncerMessageInteractor,
-                bouncerLogger,
-                selectedUserInteractor);
+        bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container));
 
         if (DeviceEntryUdfpsRefactor.isEnabled()) {
             AlternateBouncerViewBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 6528cef3..a1718b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -37,6 +37,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -45,8 +46,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.TransformableView;
@@ -898,6 +899,7 @@
         // forceUpdateVisibilities cancels outstanding animations without updating the
         // mAnimationStartVisibleType. Do so here instead.
         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+        notifySubtreeForAccessibilityContentChange();
     }
 
     private void fireExpandedVisibleListenerIfVisible() {
@@ -980,6 +982,7 @@
         // updateViewVisibilities cancels outstanding animations without updating the
         // mAnimationStartVisibleType. Do so here instead.
         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+        notifySubtreeForAccessibilityContentChange();
     }
 
     private void updateViewVisibility(int visibleType, int type, View view,
@@ -1029,6 +1032,7 @@
                     hiddenView.setVisible(false);
                 }
                 mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+                notifySubtreeForAccessibilityContentChange();
             }
         });
         fireExpandedVisibleListenerIfVisible();
@@ -1049,6 +1053,22 @@
         }
     }
 
+    @Override
+    public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+        if (isAnimatingVisibleType()) {
+            // Don't send A11y events while animating to reduce Jank.
+            return;
+        }
+        super.notifySubtreeAccessibilityStateChanged(child, source, changeType);
+    }
+
+    private void notifySubtreeForAccessibilityContentChange() {
+        if (mParent != null) {
+            mParent.notifySubtreeAccessibilityStateChanged(this, this,
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+        }
+    }
+
     /**
      * @param visibleType one of the static enum types in this view
      * @return the corresponding transformable view according to the given visible type
@@ -2277,6 +2297,11 @@
         mHeadsUpWrapper = headsUpWrapper;
     }
 
+    @VisibleForTesting
+    protected void setAnimationStartVisibleType(int animationStartVisibleType) {
+        mAnimationStartVisibleType = animationStartVisibleType;
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         try {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index 9e3c576..bd4973d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -1,5 +1,7 @@
 package com.android.systemui.biometrics.domain.model
 
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptVerticalListContentView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
@@ -23,11 +25,21 @@
         val title = "what"
         val subtitle = "a"
         val description = "request"
+        val contentView =
+            PromptVerticalListContentView.Builder()
+                .setDescription("content description")
+                .addListItem(PromptContentListItemBulletedText("content text"))
+                .build()
 
         val fpPros = fingerprintSensorPropertiesInternal().first()
         val request =
             BiometricPromptRequest.Biometric(
-                promptInfo(title = title, subtitle = subtitle, description = description),
+                promptInfo(
+                    title = title,
+                    subtitle = subtitle,
+                    description = description,
+                    contentView = contentView
+                ),
                 BiometricUserInfo(USER_ID),
                 BiometricOperationInfo(OPERATION_ID),
                 BiometricModalities(fingerprintProperties = fpPros),
@@ -36,6 +48,7 @@
         assertThat(request.title).isEqualTo(title)
         assertThat(request.subtitle).isEqualTo(subtitle)
         assertThat(request.description).isEqualTo(description)
+        assertThat(request.contentView).isEqualTo(contentView)
         assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
         assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
         assertThat(request.modalities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 6170e0c..bf61c2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -18,7 +18,10 @@
 
 import android.content.res.Configuration
 import android.graphics.Point
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
+import android.hardware.biometrics.PromptVerticalListContentView
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
@@ -60,7 +63,6 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -69,7 +71,6 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import org.mockito.Mock
-import org.mockito.Mockito.times
 import org.mockito.junit.MockitoJUnit
 
 private const val USER_ID = 4
@@ -101,6 +102,7 @@
     private lateinit var selector: PromptSelectorInteractor
     private lateinit var viewModel: PromptViewModel
     private lateinit var iconViewModel: PromptIconViewModel
+    private lateinit var promptContentView: PromptContentView
 
     @Before
     fun setup() {
@@ -136,6 +138,10 @@
         selector =
             PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
         selector.resetPrompt()
+        promptContentView =
+            PromptVerticalListContentView.Builder()
+                .addListItem(PromptContentListItemBulletedText("test"))
+                .build()
 
         viewModel =
             PromptViewModel(
@@ -1200,6 +1206,26 @@
         }
     }
 
+    @Test
+    fun descriptionOverriddenByContentView() =
+        runGenericTest(contentView = promptContentView, description = "test description") {
+            val contentView by collectLastValue(viewModel.contentView)
+            val description by collectLastValue(viewModel.description)
+
+            assertThat(description).isEqualTo("")
+            assertThat(contentView).isEqualTo(promptContentView)
+        }
+
+    @Test
+    fun descriptionWithoutContentView() =
+        runGenericTest(description = "test description") {
+            val contentView by collectLastValue(viewModel.contentView)
+            val description by collectLastValue(viewModel.description)
+
+            assertThat(description).isEqualTo("test description")
+            assertThat(contentView).isNull()
+        }
+
     /** Asserts that the selected buttons are visible now. */
     private suspend fun TestScope.assertButtonsVisible(
         tryAgain: Boolean = false,
@@ -1219,6 +1245,8 @@
     private fun runGenericTest(
         doNotStart: Boolean = false,
         allowCredentialFallback: Boolean = false,
+        description: String? = null,
+        contentView: PromptContentView? = null,
         block: suspend TestScope.() -> Unit
     ) {
         selector.initializePrompt(
@@ -1226,6 +1254,8 @@
             allowCredentialFallback = allowCredentialFallback,
             fingerprint = testCase.fingerprint,
             face = testCase.face,
+            descriptionFromApp = description,
+            contentViewFromApp = contentView,
         )
 
         // put the view model in the initial authenticating state, unless explicitly skipped
@@ -1401,11 +1431,15 @@
     face: FaceSensorPropertiesInternal? = null,
     requireConfirmation: Boolean = false,
     allowCredentialFallback: Boolean = false,
+    descriptionFromApp: String? = null,
+    contentViewFromApp: PromptContentView? = null,
 ) {
     val info =
         PromptInfo().apply {
             title = "t"
             subtitle = "s"
+            description = descriptionFromApp
+            contentView = contentViewFromApp
             authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
             isDeviceCredentialAllowed = allowCredentialFallback
             isConfirmationRequested = requireConfirmation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index e531d44..0ea4e9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -20,8 +20,13 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -50,6 +55,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
@@ -102,9 +109,12 @@
         FromPrimaryBouncerTransitionInteractor
     private lateinit var fromDreamingLockscreenHostedTransitionInteractor:
         FromDreamingLockscreenHostedTransitionInteractor
+    private lateinit var fromGlanceableHubTransitionInteractor:
+        FromGlanceableHubTransitionInteractor
 
     private lateinit var powerInteractor: PowerInteractor
     private lateinit var keyguardInteractor: KeyguardInteractor
+    private lateinit var communalInteractor: CommunalInteractor
 
     @Before
     fun setUp() {
@@ -118,10 +128,13 @@
         shadeRepository = FakeShadeRepository()
         transitionRepository = spy(FakeKeyguardTransitionRepository())
         powerInteractor = PowerInteractorFactory.create().powerInteractor
+        communalInteractor =
+            CommunalInteractorFactory.create(testScope = testScope).communalInteractor
 
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
         featureFlags = FakeFeatureFlags().apply { set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) }
+        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
 
         keyguardInteractor = createKeyguardInteractor()
 
@@ -137,6 +150,13 @@
                 )
                 .keyguardTransitionInteractor
 
+        val glanceableHubTransitions =
+            GlanceableHubTransitions(
+                testScope,
+                transitionInteractor,
+                transitionRepository,
+                communalInteractor
+            )
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                     scope = testScope,
@@ -148,6 +168,7 @@
                     flags = featureFlags,
                     shadeRepository = shadeRepository,
                     powerInteractor = powerInteractor,
+                    glanceableHubTransitions = glanceableHubTransitions,
                     inWindowLauncherUnlockAnimationInteractor = {
                         InWindowLauncherUnlockAnimationInteractor(
                             InWindowLauncherUnlockAnimationRepository(),
@@ -255,6 +276,16 @@
                     powerInteractor = powerInteractor,
                 )
                 .apply { start() }
+
+        fromGlanceableHubTransitionInteractor =
+            FromGlanceableHubTransitionInteractor(
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
+                    glanceableHubTransitions = glanceableHubTransitions,
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
     }
 
     @Test
@@ -1410,6 +1441,124 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    fun lockscreenToGlanceableHub() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to LOCKSCREEN
+            runTransitionAndSetWakefulness(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
+            runCurrent()
+
+            // WHEN a glanceable hub transition starts
+            val currentScene = CommunalSceneKey.Blank
+            val targetScene = CommunalSceneKey.Communal
+
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            communalInteractor.setTransitionState(transitionState)
+            progress.value = .1f
+            runCurrent()
+
+            // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info.ownerName)
+                .isEqualTo(FromLockscreenTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            // WHEN the user stops dragging and the glanceable hub opening is cancelled
+            clearInvocations(transitionRepository)
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+
+            // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
+            val info2 =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info2.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info2.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun glanceableHubToLockscreen() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to GLANCEABLE_HUB
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            runCurrent()
+
+            // WHEN a transition away from glanceable hub starts
+            val currentScene = CommunalSceneKey.Communal
+            val targetScene = CommunalSceneKey.Blank
+
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            communalInteractor.setTransitionState(transitionState)
+            progress.value = .1f
+            runCurrent()
+
+            // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info.ownerName)
+                .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            // WHEN the user stops dragging and the glanceable hub closing is cancelled
+            clearInvocations(transitionRepository)
+            runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+
+            // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+            val info2 =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info2.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info2.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            coroutineContext.cancelChildren()
+        }
+
     private fun createKeyguardInteractor(): KeyguardInteractor {
         return KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 233cb3d..791c080 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -56,6 +56,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
@@ -67,6 +69,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -200,6 +203,8 @@
                 new ConfigurationInteractor(configurationRepository),
                 shadeRepository,
                 () -> sceneInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
@@ -222,6 +227,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index ee7c6c8..a11839c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shade
 
 import android.content.Context
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.KeyEvent
@@ -25,50 +24,29 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardMessageAreaController
 import com.android.keyguard.KeyguardSecurityContainerController
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
-import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
-import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.bouncer.ui.binder.BouncerViewBinder
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.compose.ComposeFacade.isComposeAvailable
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
 import com.android.systemui.flags.Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION
 import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON
 import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_FEATURES
-import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler
-import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
-import com.android.systemui.log.BouncerLogger
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -86,16 +64,15 @@
 import com.android.systemui.statusbar.phone.DozeServiceHost
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
-import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.JavaAdapter
 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 java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
@@ -110,9 +87,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -133,7 +109,6 @@
     @Mock private lateinit var shadeLogger: ShadeLogger
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var ambientState: AmbientState
-    @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController
@@ -156,8 +131,6 @@
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var dragDownHelper: DragDownHelper
     @Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
-    @Mock
-    lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
     @Mock lateinit var sysUIKeyEventHandler: SysUIKeyEventHandler
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@@ -224,55 +197,18 @@
                 dumpManager,
                 pulsingGestureListener,
                 mLockscreenHostedDreamGestureListener,
-                keyguardBouncerViewModel,
-                keyguardBouncerComponentFactory,
-                mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
-                primaryBouncerToGoneTransitionViewModel,
                 mGlanceableHubContainerController,
                 notificationLaunchAnimationInteractor,
                 featureFlagsClassic,
                 fakeClock,
-                BouncerMessageInteractor(
-                    repository = BouncerMessageRepositoryImpl(),
-                    userRepository = FakeUserRepository(),
-                    countDownTimerUtil = mock(CountDownTimerUtil::class.java),
-                    updateMonitor = mock(KeyguardUpdateMonitor::class.java),
-                    biometricSettingsRepository = FakeBiometricSettingsRepository(),
-                    applicationScope = testScope.backgroundScope,
-                    trustRepository = FakeTrustRepository(),
-                    systemPropertiesHelper = mock(SystemPropertiesHelper::class.java),
-                    primaryBouncerInteractor =
-                        PrimaryBouncerInteractor(
-                            FakeKeyguardBouncerRepository(),
-                            mock(BouncerView::class.java),
-                            mock(Handler::class.java),
-                            mock(KeyguardStateController::class.java),
-                            mock(KeyguardSecurityModel::class.java),
-                            mock(PrimaryBouncerCallbackInteractor::class.java),
-                            mock(FalsingCollector::class.java),
-                            mock(DismissCallbackRegistry::class.java),
-                            context,
-                            mock(KeyguardUpdateMonitor::class.java),
-                            FakeTrustRepository(),
-                            testScope.backgroundScope,
-                            mSelectedUserInteractor,
-                            mock(DeviceEntryFaceAuthInteractor::class.java)
-                        ),
-                    facePropertyRepository = FakeFacePropertyRepository(),
-                    deviceEntryFingerprintAuthRepository =
-                        FakeDeviceEntryFingerprintAuthRepository(),
-                    faceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-                    securityModel = mock(KeyguardSecurityModel::class.java),
-                ),
-                BouncerLogger(logcatLogBuffer("BouncerLog")),
                 sysUIKeyEventHandler,
                 quickSettingsController,
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
-                mSelectedUserInteractor,
-                { mock (JavaAdapter::class.java )},
+                { mock(JavaAdapter::class.java) },
                 { mock(AlternateBouncerDependencies::class.java) },
+                mock(BouncerViewBinder::class.java)
             )
         underTest.setupExpandedStatusBar()
         underTest.setDragDownHelper(dragDownHelper)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 33d60ea..0c4bf81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -15,51 +15,28 @@
  */
 package com.android.systemui.shade
 
-import android.os.Handler
 import android.os.SystemClock
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.MotionEvent
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardMessageAreaController
 import com.android.keyguard.KeyguardSecurityContainerController
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
-import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler
-import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
-import com.android.systemui.log.BouncerLogger
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -77,11 +54,8 @@
 import com.android.systemui.statusbar.phone.DozeScrimController
 import com.android.systemui.statusbar.phone.DozeServiceHost
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -137,7 +111,6 @@
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
     @Mock
     private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
-    @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
     @Mock
@@ -150,10 +123,6 @@
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
-    @Mock
-    private lateinit var primaryBouncerToGoneTransitionViewModel:
-        PrimaryBouncerToGoneTransitionViewModel
     @Captor
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
 
@@ -217,55 +186,18 @@
                 dumpManager,
                 pulsingGestureListener,
                 mLockscreenHostedDreamGestureListener,
-                keyguardBouncerViewModel,
-                keyguardBouncerComponentFactory,
-                Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
-                primaryBouncerToGoneTransitionViewModel,
                 mGlanceableHubContainerController,
                 NotificationLaunchAnimationInteractor(NotificationLaunchAnimationRepository()),
                 featureFlags,
                 FakeSystemClock(),
-                BouncerMessageInteractor(
-                    repository = BouncerMessageRepositoryImpl(),
-                    userRepository = FakeUserRepository(),
-                    countDownTimerUtil = Mockito.mock(CountDownTimerUtil::class.java),
-                    updateMonitor = Mockito.mock(KeyguardUpdateMonitor::class.java),
-                    biometricSettingsRepository = FakeBiometricSettingsRepository(),
-                    applicationScope = testScope.backgroundScope,
-                    trustRepository = FakeTrustRepository(),
-                    systemPropertiesHelper = Mockito.mock(SystemPropertiesHelper::class.java),
-                    primaryBouncerInteractor =
-                        PrimaryBouncerInteractor(
-                            FakeKeyguardBouncerRepository(),
-                            Mockito.mock(BouncerView::class.java),
-                            Mockito.mock(Handler::class.java),
-                            Mockito.mock(KeyguardStateController::class.java),
-                            Mockito.mock(KeyguardSecurityModel::class.java),
-                            Mockito.mock(PrimaryBouncerCallbackInteractor::class.java),
-                            Mockito.mock(FalsingCollector::class.java),
-                            Mockito.mock(DismissCallbackRegistry::class.java),
-                            context,
-                            Mockito.mock(KeyguardUpdateMonitor::class.java),
-                            FakeTrustRepository(),
-                            testScope.backgroundScope,
-                            mSelectedUserInteractor,
-                            mock(),
-                        ),
-                    facePropertyRepository = FakeFacePropertyRepository(),
-                    deviceEntryFingerprintAuthRepository =
-                        FakeDeviceEntryFingerprintAuthRepository(),
-                    faceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-                    securityModel = Mockito.mock(KeyguardSecurityModel::class.java),
-                ),
-                BouncerLogger(logcatLogBuffer("BouncerLog")),
                 Mockito.mock(SysUIKeyEventHandler::class.java),
                 quickSettingsController,
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
-                mSelectedUserInteractor,
                 { Mockito.mock(JavaAdapter::class.java) },
                 { Mockito.mock(AlternateBouncerDependencies::class.java) },
+                mock()
             )
 
         controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index f0a2303..a369f82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -42,6 +42,8 @@
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
@@ -55,6 +57,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -235,6 +238,8 @@
                 new ConfigurationInteractor(configurationRepository),
                 mShadeRepository,
                 () -> sceneInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
@@ -257,6 +262,12 @@
                 featureFlags,
                 mShadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 1a6a067..4a365b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -36,6 +37,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -139,6 +141,7 @@
                 { fromLockscreenTransitionInteractor },
                 { fromPrimaryBouncerTransitionInteractor }
             )
+        val communalInteractor = CommunalInteractorFactory.create().communalInteractor
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
@@ -150,6 +153,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                GlanceableHubTransitions(
+                    testScope,
+                    keyguardTransitionInteractor,
+                    keyguardTransitionRepository,
+                    communalInteractor
+                ),
                 {
                     InWindowLauncherUnlockAnimationInteractor(
                         InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 5549fee..91e4666 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import junit.framework.Assert.assertEquals
@@ -49,6 +50,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
@@ -74,7 +77,8 @@
     @Before
     fun setup() {
         initMocks(this)
-        fakeParent = FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }
+        fakeParent =
+            spy(FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE })
         row =
             spy(
                 ExpandableNotificationRow(mContext, /* attrs= */ null).apply {
@@ -558,6 +562,35 @@
         verify(view.headsUpWrapper, never()).setAnimationsRunning(false)
     }
 
+    @Test
+    fun notifySubtreeAccessibilityStateChanged_notifiesParent() {
+        // Given: a contentView is created
+        val view = createContentView()
+        clearInvocations(fakeParent)
+
+        // When: the contentView is notified for an A11y change
+        view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
+
+        // Then: the contentView propagates the event to its parent
+        verify(fakeParent).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
+    }
+
+    @Test
+    fun notifySubtreeAccessibilityStateChanged_animatingContentView_dontNotifyParent() {
+        // Given: a collapsed contentView is created
+        val view = createContentView()
+        clearInvocations(fakeParent)
+
+        // And: it is animating to expanded
+        view.setAnimationStartVisibleType(NotificationContentView.VISIBLE_TYPE_EXPANDED)
+
+        // When: the contentView is notified for an A11y change
+        view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
+
+        // Then: the contentView DOESN'T propagates the event to its parent
+        verify(fakeParent, never()).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
+    }
+
     private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
         mock<ExpandableNotificationRow>().apply {
             whenever(this.entry).thenReturn(notificationEntry)
@@ -597,7 +630,7 @@
         }
 
     private fun createContentView(
-        isSystemExpanded: Boolean,
+        isSystemExpanded: Boolean = false,
         contractedView: View = createViewWithHeight(contractedHeight),
         expandedView: View = createViewWithHeight(expandedHeight),
         headsUpView: View = createViewWithHeight(contractedHeight),
@@ -647,5 +680,5 @@
 }
 
 private fun NotificationContentView.clearInvocations() {
-    Mockito.clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
+    clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 89842d6..f63f79f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.collectLastValue
 import com.android.systemui.collectValues
+import com.android.systemui.communal.dagger.CommunalModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -58,6 +59,7 @@
         modules =
             [
                 SysUITestModule::class,
+                CommunalModule::class,
             ]
     )
     interface TestComponent : SysUITestComponent<CollapsedStatusBarViewModelImpl> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 30434c8..8920d4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -99,6 +99,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -111,6 +113,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -439,6 +442,8 @@
                         () -> keyguardInteractor,
                         () -> mFromLockscreenTransitionInteractor,
                         () -> mFromPrimaryBouncerTransitionInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
@@ -450,6 +455,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index b28af46..fdb9b30 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -25,6 +25,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.ScreenLifecycle
@@ -111,6 +112,7 @@
     @get:Provides val zenModeController: ZenModeController = mock(),
     @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
     @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
+    @get:Provides val communalInteractor: CommunalInteractor = mock(),
 
     // log buffers
     @get:[Provides BroadcastDispatcherLog]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt
new file mode 100644
index 0000000..4a089d3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.displayStateRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.mockito.mock
+import java.util.concurrent.Executor
+
+val Kosmos.displayStateInteractor by Fixture {
+    DisplayStateInteractorImpl(
+        applicationScope = applicationCoroutineScope,
+        context = applicationContext,
+        mainExecutor = mock<Executor>(),
+        displayStateRepository = displayStateRepository,
+        displayRepository = displayRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt
new file mode 100644
index 0000000..e262066
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.fingerprintPropertyInteractor by Fixture {
+    FingerprintPropertyInteractor(
+        context = applicationContext,
+        repository = fingerprintPropertyRepository,
+        configurationInteractor = configurationInteractor,
+        displayStateInteractor = displayStateInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
index ff5179a..8010261 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -2,6 +2,7 @@
 
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
@@ -53,6 +54,7 @@
     override val alternateBouncerUIAvailable = _isAlternateBouncerUIAvailable.asStateFlow()
     private val _sideFpsShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val sideFpsShowing: StateFlow<Boolean> = _sideFpsShowing.asStateFlow()
+    override var bouncerDismissActionModel: BouncerDismissActionModel? = null
 
     override fun setPrimaryScrimmed(isScrimmed: Boolean) {
         _primaryBouncerScrimmed.value = isScrimmed
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
new file mode 100644
index 0000000..7946446
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.communalMediaRepository: CommunalMediaRepository by
+    Kosmos.Fixture { fakeCommunalMediaRepository }
+val Kosmos.fakeCommunalMediaRepository by Kosmos.Fixture { FakeCommunalMediaRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
new file mode 100644
index 0000000..be56d2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.communalRepository: CommunalRepository by Kosmos.Fixture { fakeCommunalRepository }
+val Kosmos.fakeCommunalRepository by Kosmos.Fixture { FakeCommunalRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
new file mode 100644
index 0000000..5a17f2f8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+var Kosmos.communalWidgetRepository: CommunalWidgetRepository by
+    Kosmos.Fixture { fakeCommunalWidgetRepository }
+val Kosmos.fakeCommunalWidgetRepository by
+    Kosmos.Fixture { FakeCommunalWidgetRepository(applicationCoroutineScope) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt
new file mode 100644
index 0000000..048ea3c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.displayRepository by Fixture { FakeDisplayRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt
new file mode 100644
index 0000000..4a71a09
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.display.data.repository
+
+import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.displayStateRepository by Fixture { FakeDisplayStateRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
new file mode 100644
index 0000000..59f0ec3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.appwidget.AppWidgetHost
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import org.mockito.Mockito.mock
+
+val Kosmos.communalInteractor by
+    Kosmos.Fixture {
+        CommunalInteractor(
+            communalRepository = communalRepository,
+            widgetRepository = communalWidgetRepository,
+            mediaRepository = communalMediaRepository,
+            smartspaceRepository = smartspaceRepository,
+            keyguardInteractor = keyguardInteractor,
+            appWidgetHost = mock(AppWidgetHost::class.java),
+            editWidgetsActivityStarter = mock(EditWidgetsActivityStarter::class.java),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index b1a0b67..3b38342 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -37,6 +37,7 @@
             flags = featureFlagsClassic,
             shadeRepository = shadeRepository,
             powerInteractor = powerInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
             inWindowLauncherUnlockAnimationInteractor =
                 Lazy { inWindowLauncherUnlockAnimationInteractor },
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
new file mode 100644
index 0000000..294b5ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.glanceableHubTransitions by
+    Kosmos.Fixture {
+        GlanceableHubTransitions(
+            scope = applicationCoroutineScope,
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            communalInteractor = communalInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..28fce77
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture {
+    GlanceableHubToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 933f50c..5564767 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -39,5 +39,7 @@
         screenOffAnimationController = screenOffAnimationController,
         aodBurnInViewModel = aodBurnInViewModel,
         aodAlphaViewModel = aodAlphaViewModel,
+        lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
+        glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..9fe4ea3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.lockscreenToGlanceableHubTransitionViewModel by Fixture {
+    LockscreenToGlanceableHubTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
new file mode 100644
index 0000000..e671d45
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.smartspace.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.smartspaceRepository: SmartspaceRepository by Kosmos.Fixture { fakeSmartspaceRepository }
+val Kosmos.fakeSmartspaceRepository by Kosmos.Fixture { FakeSmartspaceRepository() }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index cab2d74..5407af7 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -362,6 +362,7 @@
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
         packageFilter.addDataScheme("package");
         mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
                 packageFilter, null, mCallbackHandler);
@@ -402,6 +403,7 @@
         boolean added = false;
         boolean changed = false;
         boolean componentsModified = false;
+        int clearedUid = -1;
 
         final String pkgList[];
         switch (action) {
@@ -416,6 +418,10 @@
             case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
                 pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
                 break;
+            case Intent.ACTION_PACKAGE_DATA_CLEARED:
+                pkgList = null;
+                clearedUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+                break;
             default: {
                 Uri uri = intent.getData();
                 if (uri == null) {
@@ -430,7 +436,7 @@
                 changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
             }
         }
-        if (pkgList == null || pkgList.length == 0) {
+        if ((pkgList == null || pkgList.length == 0) && clearedUid == -1) {
             return;
         }
 
@@ -461,6 +467,8 @@
                         }
                     }
                 }
+            } else if (clearedUid != -1) {
+                componentsModified |= clearPreviewsForUidLocked(clearedUid);
             } else {
                 // If the package is being updated, we'll receive a PACKAGE_ADDED
                 // shortly, otherwise it is removed permanently.
@@ -486,6 +494,19 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private boolean clearPreviewsForUidLocked(int clearedUid) {
+        boolean changed = false;
+        final int providerCount = mProviders.size();
+        for (int i = 0; i < providerCount; i++) {
+            Provider provider = mProviders.get(i);
+            if (provider.id.uid == clearedUid) {
+                changed |= provider.clearGeneratedPreviewsLocked();
+            }
+        }
+        return changed;
+    }
+
     /**
      * Reload all widgets' masked state for the given user and its associated profiles, including
      * due to user not being available and package suspension.
@@ -3904,6 +3925,124 @@
         }
     }
 
+    @Override
+    @Nullable
+    public RemoteViews getWidgetPreview(@NonNull String callingPackage,
+            @NonNull ComponentName providerComponent, int profileId,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "getWidgetPreview() " + callingUserId);
+        }
+        mSecurityPolicy.enforceCallFromPackage(callingPackage);
+        ensureWidgetCategoryCombinationIsValid(widgetCategory);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(profileId);
+            final int providerCount = mProviders.size();
+            for (int i = 0; i < providerCount; i++) {
+                Provider provider = mProviders.get(i);
+                final ComponentName componentName = provider.id.componentName;
+                if (provider.zombie || !providerComponent.equals(componentName)) {
+                    continue;
+                }
+
+                final AppWidgetProviderInfo info = provider.getInfoLocked(mContext);
+                final int providerProfileId = info.getProfile().getIdentifier();
+                if (providerProfileId != profileId) {
+                    continue;
+                }
+
+                // Allow access to this provider if it is from the calling package or the caller has
+                // BIND_APPWIDGET permission.
+                final int callingUid = Binder.getCallingUid();
+                final String providerPackageName = componentName.getPackageName();
+                final boolean providerIsInCallerProfile =
+                        mSecurityPolicy.isProviderInCallerOrInProfileAndWhitelListed(
+                                providerPackageName, providerProfileId);
+                final boolean shouldFilterAppAccess = mPackageManagerInternal.filterAppAccess(
+                        providerPackageName, callingUid, providerProfileId);
+                final boolean providerIsInCallerPackage =
+                        mSecurityPolicy.isProviderInPackageForUid(provider, callingUid,
+                                callingPackage);
+                final boolean hasBindAppWidgetPermission =
+                        mSecurityPolicy.hasCallerBindPermissionOrBindWhiteListedLocked(
+                                callingPackage);
+                if (providerIsInCallerProfile && !shouldFilterAppAccess
+                        && (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
+                    return provider.getGeneratedPreviewLocked(widgetCategory);
+                }
+            }
+        }
+        throw new IllegalArgumentException(
+                providerComponent + " is not a valid AppWidget provider");
+    }
+
+    @Override
+    public void setWidgetPreview(@NonNull ComponentName providerComponent,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+            @NonNull RemoteViews preview) {
+        final int userId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "setWidgetPreview() " + userId);
+        }
+
+        // Make sure callers only set previews for their own package.
+        mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+        ensureWidgetCategoryCombinationIsValid(widgetCategories);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId);
+
+            final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+            final Provider provider = lookupProviderLocked(providerId);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        providerComponent + " is not a valid AppWidget provider");
+            }
+            provider.setGeneratedPreviewLocked(widgetCategories, preview);
+            scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+        }
+    }
+
+    @Override
+    public void removeWidgetPreview(@NonNull ComponentName providerComponent,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+        final int userId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "removeWidgetPreview() " + userId);
+        }
+
+        // Make sure callers only remove previews for their own package.
+        mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+        ensureWidgetCategoryCombinationIsValid(widgetCategories);
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId);
+
+            final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+            final Provider provider = lookupProviderLocked(providerId);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        providerComponent + " is not a valid AppWidget provider");
+            }
+            final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+            if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+        }
+    }
+
+    private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
+        int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+                | AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+                | AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX;
+        int invalid = ~validCategories;
+        if ((widgetCategories & invalid) != 0) {
+            throw new IllegalArgumentException(widgetCategories
+                    + " is not a valid widget category combination");
+        }
+    }
+
     private final class CallbackHandler extends Handler {
         public static final int MSG_NOTIFY_UPDATE_APP_WIDGET = 1;
         public static final int MSG_NOTIFY_PROVIDER_CHANGED = 2;
@@ -4201,6 +4340,12 @@
         ArrayList<Widget> widgets = new ArrayList<>();
         PendingIntent broadcast;
         String infoTag;
+        SparseArray<RemoteViews> generatedPreviews = new SparseArray<>(3);
+        private static final int[] WIDGET_CATEGORY_FLAGS = new int[]{
+                AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
+                AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
+                AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX,
+        };
 
         boolean zombie; // if we're in safe mode, don't prune this just because nobody references it
 
@@ -4234,7 +4379,7 @@
             return false;
         }
 
-        @GuardedBy("AppWidgetServiceImpl.mLock")
+        @GuardedBy("this.mLock")
         public AppWidgetProviderInfo getInfoLocked(Context context) {
             if (!mInfoParsed) {
                 // parse
@@ -4250,6 +4395,7 @@
                     }
                     if (newInfo != null) {
                         info = newInfo;
+                        updateGeneratedPreviewCategoriesLocked();
                     }
                 }
                 mInfoParsed = true;
@@ -4279,6 +4425,62 @@
             mInfoParsed = true;
         }
 
+        @GuardedBy("this.mLock")
+        @Nullable
+        public RemoteViews getGeneratedPreviewLocked(
+                @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+            for (int i = 0; i < generatedPreviews.size(); i++) {
+                if ((widgetCategories & generatedPreviews.keyAt(i)) != 0) {
+                    return generatedPreviews.valueAt(i);
+                }
+            }
+            return null;
+        }
+
+        @GuardedBy("this.mLock")
+        public void setGeneratedPreviewLocked(
+                @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+                @NonNull RemoteViews preview) {
+            for (int flag : WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    generatedPreviews.put(flag, preview);
+                }
+            }
+            updateGeneratedPreviewCategoriesLocked();
+        }
+
+        @GuardedBy("this.mLock")
+        public boolean removeGeneratedPreviewLocked(int widgetCategories) {
+            boolean changed = false;
+            for (int flag : WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    changed |= generatedPreviews.removeReturnOld(flag) != null;
+                }
+            }
+            if (changed) {
+                updateGeneratedPreviewCategoriesLocked();
+            }
+            return changed;
+        }
+
+        @GuardedBy("this.mLock")
+        public boolean clearGeneratedPreviewsLocked() {
+            if (generatedPreviews.size() > 0) {
+                generatedPreviews.clear();
+                updateGeneratedPreviewCategoriesLocked();
+                return true;
+            }
+            return false;
+        }
+
+        @GuardedBy("this.mLock")
+        private void updateGeneratedPreviewCategoriesLocked() {
+            info.generatedPreviewCategories = 0;
+            for (int i = 0; i < generatedPreviews.size(); i++) {
+                info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+            }
+        }
+
         @Override
         public String toString() {
             return "Provider{" + id + (zombie ? " Z" : "") + '}';
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6a81425..a856f42 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -4271,13 +4271,19 @@
                 if (value != null) {
                     viewState.setCurrentValue(value);
                 }
-
+                boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0;
                 if (shouldRequestSecondaryProvider(flags)) {
                     if (requestNewFillResponseOnViewEnteredIfNecessaryLocked(
                             id, viewState, flags)) {
                         Slog.v(TAG, "Started a new fill request for secondary provider.");
                         return;
                     }
+
+                    FillResponse response = viewState.getSecondaryResponse();
+                    if (response != null) {
+                        logPresentationStatsOnViewEntered(response, isCredmanRequested);
+                    }
+
                     // If the ViewState is ready to be displayed, onReady() will be called.
                     viewState.update(value, virtualBounds, flags);
 
@@ -4363,15 +4369,9 @@
                     return;
                 }
 
-                if (viewState.getResponse() != null) {
-                    boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0;
-                    FillResponse response = viewState.getResponse();
-                    mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId());
-                    mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested);
-                    mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId(
-                            mFieldClassificationIdSnapshot);
-                    mPresentationStatsEventLogger.maybeSetAvailableCount(
-                            response.getDatasets(), mCurrentViewId);
+                FillResponse response = viewState.getResponse();
+                if (response != null) {
+                    logPresentationStatsOnViewEntered(response, isCredmanRequested);
                 }
 
                 if (isSameViewEntered) {
@@ -4412,6 +4412,17 @@
     }
 
     @GuardedBy("mLock")
+    private void logPresentationStatsOnViewEntered(FillResponse response,
+            boolean isCredmanRequested) {
+        mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId());
+        mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested);
+        mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId(
+                mFieldClassificationIdSnapshot);
+        mPresentationStatsEventLogger.maybeSetAvailableCount(
+                response.getDatasets(), mCurrentViewId);
+    }
+
+    @GuardedBy("mLock")
     private void hideAugmentedAutofillLocked(@NonNull ViewState viewState) {
         if ((viewState.getState()
                 & ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 25ec683..4db9ead 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4869,8 +4869,8 @@
 
                     final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
                             .getSortedInputMethodAndSubtypeList(
-                                    showAuxSubtypes, isScreenLocked, false, mContext,
-                                    mMethodMap, mSettings.getCurrentUserId());
+                                    showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
+                                    mContext, mMethodMap, mSettings.getCurrentUserId());
                     mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
                             lastInputMethodId, lastInputMethodSubtypeId, imList);
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index fb57c09..58a68f2a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -255,14 +255,6 @@
             return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
         }
 
-        private void putBoolean(String key, boolean value) {
-            SecureSettingsWrapper.putBoolean(key, value, mCurrentUserId);
-        }
-
-        private boolean getBoolean(String key, boolean defaultValue) {
-            return SecureSettingsWrapper.getBoolean(key, defaultValue, mCurrentUserId);
-        }
-
         ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
             return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
         }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2128c991..e226953 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -139,6 +139,7 @@
 import com.android.server.power.batterysaver.BatterySaverPolicy;
 import com.android.server.power.batterysaver.BatterySaverStateMachine;
 import com.android.server.power.batterysaver.BatterySavingStats;
+import com.android.server.power.feature.PowerManagerFlags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -329,6 +330,8 @@
     // True if battery saver is supported on this device.
     private final boolean mBatterySaverSupported;
 
+    private final PowerManagerFlags mFeatureFlags;
+
     private boolean mDisableScreenWakeLocksWhileCached;
 
     private LightsManager mLightsManager;
@@ -1079,6 +1082,10 @@
         DeviceConfigParameterProvider createDeviceConfigParameterProvider() {
             return new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
         }
+
+        PowerManagerFlags getFlags() {
+            return new PowerManagerFlags();
+        }
     }
 
     /** Interface for checking an app op permission */
@@ -1145,6 +1152,7 @@
         mNativeWrapper = injector.createNativeWrapper();
         mSystemProperties = injector.createSystemPropertiesWrapper();
         mClock = injector.createClock();
+        mFeatureFlags = injector.getFlags();
         mInjector = injector;
 
         mHandlerThread = new ServiceThread(TAG,
@@ -4802,6 +4810,7 @@
         mAmbientDisplaySuppressionController.dump(pw);
 
         mLowPowerStandbyController.dump(pw);
+        mFeatureFlags.dump(pw);
     }
 
     private void dumpProto(FileDescriptor fd) {
diff --git a/services/core/java/com/android/server/power/feature/Android.bp b/services/core/java/com/android/server/power/feature/Android.bp
new file mode 100644
index 0000000..2295b41
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/Android.bp
@@ -0,0 +1,7 @@
+aconfig_declarations {
+    name: "power_flags",
+    package: "com.android.server.power.feature.flags",
+    srcs: [
+        "*.aconfig",
+    ],
+}
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
new file mode 100644
index 0000000..a5a7069
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -0,0 +1,91 @@
+/*
+ * 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.power.feature;
+
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.server.power.feature.flags.Flags;
+
+import java.io.PrintWriter;
+import java.util.function.Supplier;
+
+/**
+ * Utility class to read the flags used in the power manager server.
+ */
+public class PowerManagerFlags {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "PowerManagerFlags";
+
+    private final FlagState mEarlyScreenTimeoutDetectorFlagState = new FlagState(
+            Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR,
+            Flags::enableEarlyScreenTimeoutDetector);
+
+    /** Returns whether early-screen-timeout-detector is enabled on not. */
+    public boolean isEarlyScreenTimeoutDetectorEnabled() {
+        return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
+    }
+
+    /**
+     * dumps all flagstates
+     * @param pw printWriter
+     */
+    public void dump(PrintWriter pw) {
+        pw.println("PowerManagerFlags:");
+        pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
+    }
+
+    private static class FlagState {
+
+        private final String mName;
+
+        private final Supplier<Boolean> mFlagFunction;
+        private boolean mEnabledSet;
+        private boolean mEnabled;
+
+        private FlagState(String name, Supplier<Boolean> flagFunction) {
+            mName = name;
+            mFlagFunction = flagFunction;
+        }
+
+        private boolean isEnabled() {
+            if (mEnabledSet) {
+                if (DEBUG) {
+                    Slog.d(TAG, mName + ": mEnabled. Recall = " + mEnabled);
+                }
+                return mEnabled;
+            }
+            mEnabled = mFlagFunction.get();
+            if (DEBUG) {
+                Slog.d(TAG, mName + ": mEnabled. Flag value = " + mEnabled);
+            }
+            mEnabledSet = true;
+            return mEnabled;
+        }
+
+        @Override
+        public String toString() {
+            // remove com.android.server.power.feature.flags. from the beginning of the name.
+            // align all isEnabled() values.
+            // Adjust lengths if we end up with longer names
+            final int nameLength = mName.length();
+            return TextUtils.substring(mName,  39, nameLength) + ": "
+                    + TextUtils.formatSimple("%" + (91 - nameLength) + "s%s", " " , isEnabled())
+                    + " (def:" + mFlagFunction.get() + ")";
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
new file mode 100644
index 0000000..c8c16db
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.server.power.feature.flags"
+
+# Important: Flags must be accessed through PowerManagerFlags.
+
+flag {
+    name: "enable_early_screen_timeout_detector"
+    namespace: "power_manager"
+    description: "Feature flag for Early Screen Timeout detector"
+    bug: "309861917"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index fcc0de1..3094b18 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1910,6 +1910,12 @@
                 // Transforms do not need to be persisted; the IkeSession will keep them alive
                 mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform);
 
+                if (direction == IpSecManager.DIRECTION_IN
+                        && mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                        && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                    mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform);
+                }
+
                 // For inbound transforms, additionally allow forwarded traffic to bridge to DUN (as
                 // needed)
                 final Set<Integer> exposedCaps = mConnectionConfig.getAllExposedCapabilities();
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 48df44b..3f8d39e 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -30,6 +30,7 @@
 import android.annotation.Nullable;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -52,6 +53,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 import com.android.server.vcn.util.LogUtils;
 
 import java.util.ArrayList;
@@ -201,6 +203,14 @@
         NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
+
+        if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+                evaluator.close();
+            }
+        }
+
         mUnderlyingNetworkRecords.clear();
 
         // Register new callbacks. Make-before-break; always register new callbacks before removal
@@ -417,11 +427,42 @@
         if (oldSnapshot
                 .getAllSubIdsInGroup(mSubscriptionGroup)
                 .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
+
+            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                reevaluateNetworks();
+            }
             return;
         }
         registerOrUpdateNetworkRequests();
     }
 
+    /**
+     * Pass the IpSecTransform of the VCN to UnderlyingNetworkController for metric monitoring
+     *
+     * <p>Caller MUST call it when IpSecTransforms have been created for VCN creation or migration
+     */
+    public void updateInboundTransform(
+            @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) {
+        if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            logWtf("#updateInboundTransform: unexpected call; flags missing");
+            return;
+        }
+
+        Objects.requireNonNull(currentNetwork, "currentNetwork is null");
+        Objects.requireNonNull(transform, "transform is null");
+
+        if (mCurrentRecord == null
+                || mRouteSelectionCallback == null
+                || !Objects.equals(currentNetwork.network, mCurrentRecord.network)) {
+            // The caller (VcnGatewayConnection) is out-of-dated. Ignore this call.
+            return;
+        }
+
+        mUnderlyingNetworkRecords.get(mCurrentRecord.network).setInboundTransform(transform);
+    }
+
     /** Tears down this Tracker, and releases all underlying network requests. */
     public void teardown() {
         mVcnContext.ensureRunningOnLooperThread();
@@ -438,7 +479,7 @@
 
     private TreeSet<UnderlyingNetworkEvaluator> getSortedUnderlyingNetworks() {
         TreeSet<UnderlyingNetworkEvaluator> sorted =
-                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator());
+                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator(mVcnContext));
 
         for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
             if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) {
@@ -525,11 +566,17 @@
                             mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
                             mSubscriptionGroup,
                             mLastSnapshot,
-                            mCarrierConfig));
+                            mCarrierConfig,
+                            new NetworkEvaluatorCallbackImpl()));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
+            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                mUnderlyingNetworkRecords.get(network).close();
+            }
+
             mUnderlyingNetworkRecords.remove(network);
 
             reevaluateNetworks();
@@ -598,6 +645,21 @@
         }
     }
 
+    @VisibleForTesting
+    class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback {
+        @Override
+        public void onEvaluationResultChanged() {
+            if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                logWtf("#onEvaluationResultChanged: unexpected call; flags missing");
+                return;
+            }
+
+            mVcnContext.ensureRunningOnLooperThread();
+            reevaluateNetworks();
+        }
+    }
+
     private String getLogPrefix() {
         return "("
                 + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup)
@@ -690,21 +752,22 @@
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public static class Dependencies {
-        /** Construct a new UnderlyingNetworkEvaluator */
         public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator(
                 @NonNull VcnContext vcnContext,
                 @NonNull Network network,
                 @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
                 @NonNull ParcelUuid subscriptionGroup,
                 @NonNull TelephonySubscriptionSnapshot lastSnapshot,
-                @Nullable PersistableBundleWrapper carrierConfig) {
+                @Nullable PersistableBundleWrapper carrierConfig,
+                @NonNull NetworkEvaluatorCallback evaluatorCallback) {
             return new UnderlyingNetworkEvaluator(
                     vcnContext,
                     network,
                     underlyingNetworkTemplates,
                     subscriptionGroup,
                     lastSnapshot,
-                    carrierConfig);
+                    carrierConfig,
+                    evaluatorCallback);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
index c124a19..2f4cf5e 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -16,23 +16,32 @@
 
 package com.android.server.vcn.routeselection;
 
+import static com.android.server.VcnManagementService.LOCAL_LOG;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.Handler;
 import android.os.ParcelUuid;
+import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
 
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 /**
  * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for
@@ -43,20 +52,41 @@
 public class UnderlyingNetworkEvaluator {
     private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName();
 
+    private static final int[] PENALTY_TIMEOUT_MINUTES_DEFAULT = new int[] {5};
+
     @NonNull private final VcnContext mVcnContext;
+    @NonNull private final Handler mHandler;
+    @NonNull private final Object mCancellationToken = new Object();
+
     @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder;
 
+    @NonNull private final NetworkEvaluatorCallback mEvaluatorCallback;
+    @NonNull private final List<NetworkMetricMonitor> mMetricMonitors = new ArrayList<>();
+
+    @NonNull private final Dependencies mDependencies;
+
+    // TODO: Support back-off timeouts
+    private long mPenalizedTimeoutMs;
+
     private boolean mIsSelected;
+    private boolean mIsPenalized;
     private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
 
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     public UnderlyingNetworkEvaluator(
             @NonNull VcnContext vcnContext,
             @NonNull Network network,
             @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot lastSnapshot,
-            @Nullable PersistableBundleWrapper carrierConfig) {
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkEvaluatorCallback evaluatorCallback,
+            @NonNull Dependencies dependencies) {
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mHandler = new Handler(mVcnContext.getLooper());
+
+        mDependencies = Objects.requireNonNull(dependencies, "Missing dependencies");
+        mEvaluatorCallback = Objects.requireNonNull(evaluatorCallback, "Missing deps");
 
         Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates");
         Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
@@ -66,9 +96,76 @@
                 new UnderlyingNetworkRecord.Builder(
                         Objects.requireNonNull(network, "Missing network"));
         mIsSelected = false;
+        mIsPenalized = false;
+        mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig);
 
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        if (isIpSecPacketLossDetectorEnabled()) {
+            try {
+                mMetricMonitors.add(
+                        mDependencies.newIpSecPacketLossDetector(
+                                mVcnContext,
+                                mNetworkRecordBuilder.getNetwork(),
+                                carrierConfig,
+                                new MetricMonitorCallbackImpl()));
+            } catch (IllegalAccessException e) {
+                // No action. Do not add anything to mMetricMonitors
+            }
+        }
+    }
+
+    public UnderlyingNetworkEvaluator(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkEvaluatorCallback evaluatorCallback) {
+        this(
+                vcnContext,
+                network,
+                underlyingNetworkTemplates,
+                subscriptionGroup,
+                lastSnapshot,
+                carrierConfig,
+                evaluatorCallback,
+                new Dependencies());
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        /** Get an IpSecPacketLossDetector instance */
+        public IpSecPacketLossDetector newIpSecPacketLossDetector(
+                @NonNull VcnContext vcnContext,
+                @NonNull Network network,
+                @Nullable PersistableBundleWrapper carrierConfig,
+                @NonNull NetworkMetricMonitor.NetworkMetricMonitorCallback callback)
+                throws IllegalAccessException {
+            return new IpSecPacketLossDetector(vcnContext, network, carrierConfig, callback);
+        }
+    }
+
+    /** Callback to notify caller to reevaluate network selection */
+    public interface NetworkEvaluatorCallback {
+        /**
+         * Called when mIsPenalized changed
+         *
+         * <p>When receiving this call, UnderlyingNetworkController should reevaluate all network
+         * candidates for VCN underlying network selection
+         */
+        void onEvaluationResultChanged();
+    }
+
+    private class MetricMonitorCallbackImpl
+            implements NetworkMetricMonitor.NetworkMetricMonitorCallback {
+        public void onValidationResultReceived() {
+            mVcnContext.ensureRunningOnLooperThread();
+
+            handleValidationResult();
+        }
     }
 
     private void updatePriorityClass(
@@ -91,8 +188,25 @@
         }
     }
 
-    public static Comparator<UnderlyingNetworkEvaluator> getComparator() {
+    private boolean isIpSecPacketLossDetectorEnabled() {
+        return isIpSecPacketLossDetectorEnabled(mVcnContext);
+    }
+
+    private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) {
+        return vcnContext.isFlagIpSecTransformStateEnabled()
+                && vcnContext.isFlagNetworkMetricMonitorEnabled();
+    }
+
+    /** Get the comparator for UnderlyingNetworkEvaluator */
+    public static Comparator<UnderlyingNetworkEvaluator> getComparator(VcnContext vcnContext) {
         return (left, right) -> {
+            if (isIpSecPacketLossDetectorEnabled(vcnContext)) {
+                if (left.mIsPenalized != right.mIsPenalized) {
+                    // A penalized network should have lower priority which means a larger index
+                    return left.mIsPenalized ? 1 : -1;
+                }
+            }
+
             final int leftIndex = left.mPriorityClass;
             final int rightIndex = right.mPriorityClass;
 
@@ -112,6 +226,64 @@
         };
     }
 
+    private static long getPenaltyTimeoutMs(@Nullable PersistableBundleWrapper carrierConfig) {
+        final int[] timeoutMinuteList;
+
+        if (carrierConfig != null) {
+            timeoutMinuteList =
+                    carrierConfig.getIntArray(
+                            VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
+                            PENALTY_TIMEOUT_MINUTES_DEFAULT);
+        } else {
+            timeoutMinuteList = PENALTY_TIMEOUT_MINUTES_DEFAULT;
+        }
+
+        // TODO: Add the support of back-off timeouts and return the full list
+        return TimeUnit.MINUTES.toMillis(timeoutMinuteList[0]);
+    }
+
+    private void handleValidationResult() {
+        final boolean wasPenalized = mIsPenalized;
+        mIsPenalized = false;
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            mIsPenalized |= monitor.isValidationFailed();
+        }
+
+        if (wasPenalized == mIsPenalized) {
+            return;
+        }
+
+        logInfo(
+                "#handleValidationResult: wasPenalized "
+                        + wasPenalized
+                        + " mIsPenalized "
+                        + mIsPenalized);
+
+        if (mIsPenalized) {
+            mHandler.postDelayed(
+                    new ExitPenaltyBoxRunnable(), mCancellationToken, mPenalizedTimeoutMs);
+        } else {
+            // Exit the penalty box
+            mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+        }
+        mEvaluatorCallback.onEvaluationResultChanged();
+    }
+
+    public class ExitPenaltyBoxRunnable implements Runnable {
+        @Override
+        public void run() {
+            if (!mIsPenalized) {
+                logWtf("Evaluator not being penalized but ExitPenaltyBoxRunnable was scheduled");
+                return;
+            }
+
+            // TODO: There might be a future metric monitor (e.g. ping) that will require the
+            // validation to pass before exiting the penalty box.
+            mIsPenalized = false;
+            mEvaluatorCallback.onEvaluationResultChanged();
+        }
+    }
+
     /** Set the NetworkCapabilities */
     public void setNetworkCapabilities(
             @NonNull NetworkCapabilities nc,
@@ -162,6 +334,10 @@
 
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setIsSelectedUnderlyingNetwork(isSelected);
+        }
     }
 
     /**
@@ -174,6 +350,35 @@
             @Nullable PersistableBundleWrapper carrierConfig) {
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        // The already scheduled event will not be affected. The followup events will be scheduled
+        // with the new timeout
+        mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setCarrierConfig(carrierConfig);
+        }
+    }
+
+    /** Update the inbound IpSecTransform applied to the network */
+    public void setInboundTransform(@NonNull IpSecTransform transform) {
+        if (!mIsSelected) {
+            logWtf("setInboundTransform on an unselected evaluator");
+            return;
+        }
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setInboundTransform(transform);
+        }
+    }
+
+    /** Close the evaluator and stop all the underlying network metric monitors */
+    public void close() {
+        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.close();
+        }
     }
 
     /** Return whether this network evaluator is valid */
@@ -196,6 +401,11 @@
         return mPriorityClass;
     }
 
+    /** Return whether the network is being penalized */
+    public boolean isPenalized() {
+        return mIsPenalized;
+    }
+
     /** Dump the information of this instance */
     public void dump(IndentingPrintWriter pw) {
         pw.println("UnderlyingNetworkEvaluator:");
@@ -211,7 +421,22 @@
 
         pw.println("mIsSelected: " + mIsSelected);
         pw.println("mPriorityClass: " + mPriorityClass);
+        pw.println("mIsPenalized: " + mIsPenalized);
 
         pw.decreaseIndent();
     }
+
+    private String getLogPrefix() {
+        return "[Network " + mNetworkRecordBuilder.getNetwork() + "] ";
+    }
+
+    private void logInfo(String msg) {
+        Slog.i(TAG, getLogPrefix() + msg);
+        LOCAL_LOG.log("[INFO ] " + TAG + getLogPrefix() + msg);
+    }
+
+    private void logWtf(String msg) {
+        Slog.wtf(TAG, getLogPrefix() + msg);
+        LOCAL_LOG.log("[WTF ] " + TAG + getLogPrefix() + msg);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 59d0210..0da0bb4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1034,8 +1034,14 @@
                 launchOpts.remove(WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
                 final SafeActivityOptions safeOptions =
                         SafeActivityOptions.fromBundle(launchOpts, caller.mPid, caller.mUid);
+                if (transition != null) {
+                    transition.deferTransitionReady();
+                }
                 waitAsyncStart(() -> mService.mTaskSupervisor.startActivityFromRecents(
                         caller.mPid, caller.mUid, taskId, safeOptions));
+                if (transition != null) {
+                    transition.continueTransitionReady();
+                }
                 break;
             }
             case HIERARCHY_OP_TYPE_REORDER:
@@ -1113,11 +1119,17 @@
                     activityOptions.setCallerDisplayId(DEFAULT_DISPLAY);
                 }
                 final Bundle options = activityOptions != null ? activityOptions.toBundle() : null;
+                if (transition != null) {
+                    transition.deferTransitionReady();
+                }
                 int res = waitAsyncStart(() -> mService.mAmInternal.sendIntentSender(
                         hop.getPendingIntent().getTarget(),
                         hop.getPendingIntent().getWhitelistToken(), 0 /* code */,
                         hop.getActivityIntent(), resolvedType, null /* finishReceiver */,
                         null /* requiredPermission */, options));
+                if (transition != null) {
+                    transition.continueTransitionReady();
+                }
                 if (ActivityManager.isStartResultSuccessful(res)) {
                     effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 6b85a32..95a9610 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -25,25 +25,15 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.IContentProvider;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.os.Build;
 import android.os.LocaleList;
 import android.os.Parcel;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.test.mock.MockContentResolver;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IntArray;
@@ -1243,63 +1233,6 @@
                         imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr)));
     }
 
-    private static TestContext createMockContext(int userId) {
-        return new TestContext(InstrumentationRegistry.getInstrumentation()
-                .getTargetContext(), userId);
-    }
-
-    private static class TestContext extends ContextWrapper {
-        private int mUserId;
-        private ContentResolver mResolver;
-        private Resources mResources;
-
-        private static TestContext sSecondaryUserContext;
-
-        TestContext(@NonNull Context context, int userId) {
-            super(context);
-            mUserId = userId;
-            mResolver = mock(MockContentResolver.class);
-            when(mResolver.acquireProvider(Settings.Secure.CONTENT_URI)).thenReturn(
-                    mock(IContentProvider.class));
-            mResources = mock(Resources.class);
-
-            final Configuration configuration = new Configuration();
-            if (userId == 0) {
-                configuration.setLocale(LOCALE_EN_US);
-            } else {
-                configuration.setLocale(LOCALE_FR_CA);
-            }
-            doReturn(configuration).when(mResources).getConfiguration();
-        }
-
-        @Override
-        public Context createContextAsUser(UserHandle user, int flags) {
-            if (user.getIdentifier() != UserHandle.USER_SYSTEM) {
-                return sSecondaryUserContext = new TestContext(this, user.getIdentifier());
-            }
-            return this;
-        }
-
-        @Override
-        public int getUserId() {
-            return mUserId;
-        }
-
-        @Override
-        public ContentResolver getContentResolver() {
-            return mResolver;
-        }
-
-        @Override
-        public Resources getResources() {
-            return mResources;
-        }
-
-        static Context getSecondaryUserContext() {
-            return sSecondaryUserContext;
-        }
-    }
-
     private static void verifySplitEnabledImeStr(@NonNull String enabledImeStr,
             @NonNull String... expected) {
         final ArrayList<String> actual = new ArrayList<>();
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 79e4ad0..73c26a3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9683,6 +9683,17 @@
             "parameters_used_for_ntn_lte_signal_bar_int";
 
     /**
+     * Indicating whether plmns associated with carrier satellite can be exposed to user when
+     * manually scanning available cellular network.
+     * If key is {@code true}, satellite plmn should not be exposed to user and should be
+     * automatically set, {@code false} otherwise. Default value is {@code true}.
+     *
+     * @hide
+     */
+    public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL =
+            "remove_satellite_plmn_in_manual_network_scan_bool";
+
+    /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
      * the default APN (i.e. internet) will be used for tethering.
      *
@@ -10787,6 +10798,7 @@
                 });
         sDefaults.putInt(KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT,
                 CellSignalStrengthLte.USE_RSRP);
+        sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true);
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0dce084..e4ea479 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6445,10 +6445,6 @@
      * targeting API level 31+.
      *
      * @return the current call state.
-     *
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
-     *
      * @deprecated Use {@link #getCallStateForSubscription} to retrieve the call state for a
      * specific telephony subscription (which allows carrier privileged apps),
      * {@link TelephonyCallback.CallStateListener} for real-time call state updates, or
@@ -6456,7 +6452,6 @@
      * device.
      */
     @RequiresPermission(value = android.Manifest.permission.READ_PHONE_STATE, conditional = true)
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     @Deprecated
     public @CallState int getCallState() {
         if (mContext != null) {
@@ -10782,9 +10777,7 @@
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
-     * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
+   * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
      * @hide
      */
     @Deprecated
@@ -10793,15 +10786,12 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isOffhook() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return tm.isInCall();
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      * @deprecated Use {@link android.telecom.TelecomManager#isRinging} instead
      * @hide
      */
@@ -10811,15 +10801,12 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isRinging() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return tm.isRinging();
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
      * @hide
      */
@@ -10829,7 +10816,6 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isIdle() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return !tm.isInCall();
@@ -12044,11 +12030,8 @@
      *
      * @return {@code true} if the device supports TTY mode, and {@code false} otherwise.
      *
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      */
     @Deprecated
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isTtyModeSupported() {
         try {
             TelecomManager telecomManager = null;
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 2a69703..3b0397b 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -49,8 +49,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -2142,6 +2144,35 @@
         }
     }
 
+    /**
+     * Get all satellite PLMNs for which attach is enable for carrier.
+     *
+     * @param subId subId The subscription ID of the carrier.
+     *
+     * @return List of plmn for carrier satellite service. If no plmn is available, empty list will
+     * be returned.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @NonNull public List<String> getAllSatellitePlmnsForCarrier(int subId) {
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid subscription ID");
+        }
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.getAllSatellitePlmnsForCarrier(subId);
+            } else {
+                throw new IllegalStateException("Telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("getAllSatellitePlmnsForCarrier() RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return new ArrayList<>();
+    }
+
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
                 .getTelephonyServiceManager()
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 3ea86c7..acbf354 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3258,4 +3258,17 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
     boolean isNullCipherNotificationsEnabled();
+
+    /**
+     * Get the aggregated satellite plmn list. This API collects plmn data from multiple sources,
+     * including carrier config, entitlement server, and config update.
+     *
+     * @param subId subId The subscription ID of the carrier.
+     *
+     * @return List of plmns for carrier satellite service. If no plmn is available, empty list will
+     * be returned.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    List<String> getAllSatellitePlmnsForCarrier(int subId);
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index f846164..20b7f1f 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -269,6 +269,7 @@
     @Test
     public void testCreatedTransformsAreApplied() throws Exception {
         verifyVcnTransformsApplied(mGatewayConnection, false /* expectForwardTransform */);
+        verify(mUnderlyingNetworkController).updateInboundTransform(any(), any());
     }
 
     @Test
@@ -327,6 +328,8 @@
                             eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any());
         }
 
+        verify(mUnderlyingNetworkController).updateInboundTransform(any(), any());
+
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
 
         final List<ChildSaProposal> saProposals =
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 4c7b25a..e29e462 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -223,6 +223,8 @@
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
         doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
+        doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
+        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 355c221..6015e931 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -26,6 +26,8 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.net.IpSecConfig;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -141,4 +143,8 @@
                         mock(Handler.class));
         setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class);
     }
+
+    protected IpSecTransform makeDummyIpSecTransform() throws Exception {
+        return new IpSecTransform(mContext, new IpSecConfig());
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 992f102..588624b 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -47,6 +47,8 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.IpSecConfig;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -70,6 +72,7 @@
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -153,11 +156,13 @@
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
     @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb;
+    @Mock private NetworkEvaluatorCallback mEvaluatorCallback;
     @Mock private Network mNetwork;
 
     @Spy private Dependencies mDependencies = new Dependencies();
 
     @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
+    @Captor private ArgumentCaptor<NetworkEvaluatorCallback> mEvaluatorCallbackCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
@@ -176,7 +181,7 @@
                                 mTestLooper.getLooper(),
                                 mVcnNetworkProvider,
                                 false /* isInTestMode */));
-        resetVcnContext();
+        resetVcnContext(mVcnContext);
 
         setupSystemService(
                 mContext,
@@ -202,10 +207,11 @@
                                         .getVcnUnderlyingNetworkPriorities(),
                                 SUB_GROUP,
                                 mSubscriptionSnapshot,
-                                null));
+                                null,
+                                mEvaluatorCallback));
         doReturn(mNetworkEvaluator)
                 .when(mDependencies)
-                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any());
 
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
@@ -217,9 +223,11 @@
                         mDependencies);
     }
 
-    private void resetVcnContext() {
-        reset(mVcnContext);
-        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+    private void resetVcnContext(VcnContext vcnContext) {
+        reset(vcnContext);
+        doNothing().when(vcnContext).ensureRunningOnLooperThread();
+        doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled();
+        doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled();
     }
 
     // Package private for use in NetworkPriorityClassifierTest
@@ -245,11 +253,13 @@
         final ConnectivityManager cm = mock(ConnectivityManager.class);
         setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
         final VcnContext vcnContext =
-                new VcnContext(
-                        mContext,
-                        mTestLooper.getLooper(),
-                        mVcnNetworkProvider,
-                        true /* isInTestMode */);
+                spy(
+                        new VcnContext(
+                                mContext,
+                                mTestLooper.getLooper(),
+                                mVcnNetworkProvider,
+                                true /* isInTestMode */));
+        resetVcnContext(vcnContext);
 
         new UnderlyingNetworkController(
                 vcnContext,
@@ -554,6 +564,45 @@
         verify(mNetworkEvaluator).reevaluate(any(), any(), any(), any());
     }
 
+    @Test
+    public void testUpdateIpSecTransform() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        final UnderlyingNetworkRecord expectedRecord =
+                getTestNetworkRecord(
+                        mNetwork,
+                        INITIAL_NETWORK_CAPABILITIES,
+                        INITIAL_LINK_PROPERTIES,
+                        false /* isBlocked */);
+        final IpSecTransform expectedTransform = new IpSecTransform(mContext, new IpSecConfig());
+
+        mUnderlyingNetworkController.updateInboundTransform(expectedRecord, expectedTransform);
+        verify(mNetworkEvaluator).setInboundTransform(expectedTransform);
+    }
+
+    @Test
+    public void testOnEvaluationResultChanged() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        // Verify #reevaluateNetworks is called by checking #getNetworkRecord
+        verify(mNetworkEvaluator).getNetworkRecord();
+
+        // Trigger the callback
+        verify(mDependencies)
+                .newUnderlyingNetworkEvaluator(
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        mEvaluatorCallbackCaptor.capture());
+        mEvaluatorCallbackCaptor.getValue().onEvaluationResultChanged();
+
+        // Verify #reevaluateNetworks is called again
+        verify(mNetworkEvaluator, times(2)).getNetworkRecord();
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
@@ -682,7 +731,7 @@
 
         cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */);
 
-        verifyOnSelectedUnderlyingNetworkChanged(null);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null);
     }
 
     @Test
@@ -690,6 +739,7 @@
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onLost(mNetwork);
+        verify(mNetworkEvaluator).close();
 
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null);
     }
@@ -755,10 +805,11 @@
                                 underlyingNetworkTemplates,
                                 SUB_GROUP,
                                 mSubscriptionSnapshot,
-                                null));
+                                null,
+                                mEvaluatorCallback));
         doReturn(evaluator)
                 .when(mDependencies)
-                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any());
 
         cb.onAvailable(network);
         cb.onCapabilitiesChanged(network, responseNetworkCaps);
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
index 985e70c..aa81efe 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -16,27 +16,65 @@
 
 package com.android.server.vcn.routeselection;
 
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY;
+
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.net.IpSecTransform;
 import android.net.vcn.VcnGatewayConnectionConfig;
-import android.os.PersistableBundle;
+
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.concurrent.TimeUnit;
 
 public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
-    private PersistableBundleWrapper mCarrierConfig;
+    private static final int PENALTY_TIMEOUT_MIN = 10;
+    private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN);
+
+    @Mock private PersistableBundleWrapper mCarrierConfig;
+    @Mock private IpSecPacketLossDetector mIpSecPacketLossDetector;
+    @Mock private Dependencies mDependencies;
+    @Mock private NetworkEvaluatorCallback mEvaluatorCallback;
+
+    @Captor private ArgumentCaptor<NetworkMetricMonitorCallback> mMetricMonitorCbCaptor;
+
+    private UnderlyingNetworkEvaluator mNetworkEvaluator;
 
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle());
+
+        when(mDependencies.newIpSecPacketLossDetector(any(), any(), any(), any()))
+                .thenReturn(mIpSecPacketLossDetector);
+
+        when(mCarrierConfig.getIntArray(
+                        eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject()))
+                .thenReturn(new int[] {PENALTY_TIMEOUT_MIN});
+
+        mNetworkEvaluator = newValidUnderlyingNetworkEvaluator();
     }
 
     private UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator() {
@@ -46,7 +84,34 @@
                 VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
                 SUB_GROUP,
                 mSubscriptionSnapshot,
+                mCarrierConfig,
+                mEvaluatorCallback,
+                mDependencies);
+    }
+
+    private UnderlyingNetworkEvaluator newValidUnderlyingNetworkEvaluator() {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+
+        evaluator.setNetworkCapabilities(
+                CELL_NETWORK_CAPABILITIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
                 mCarrierConfig);
+        evaluator.setLinkProperties(
+                LINK_PROPERTIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setIsBlocked(
+                false /* isBlocked */,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+
+        return evaluator;
     }
 
     @Test
@@ -98,4 +163,174 @@
         assertEquals(2, evaluator.getPriorityClass());
         assertEquals(expectedRecord, evaluator.getNetworkRecord());
     }
+
+    private void checkSetSelectedNetwork(boolean isSelected) {
+        mNetworkEvaluator.setIsSelected(
+                isSelected,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        verify(mIpSecPacketLossDetector).setIsSelectedUnderlyingNetwork(isSelected);
+    }
+
+    @Test
+    public void testSetIsSelected_selected() throws Exception {
+        checkSetSelectedNetwork(true /* isSelectedExpected */);
+    }
+
+    @Test
+    public void testSetIsSelected_unselected() throws Exception {
+        checkSetSelectedNetwork(false /* isSelectedExpected */);
+    }
+
+    @Test
+    public void testSetIpSecTransform_onSelectedNetwork() throws Exception {
+        final IpSecTransform transform = makeDummyIpSecTransform();
+
+        // Make the network selected
+        mNetworkEvaluator.setIsSelected(
+                true,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        mNetworkEvaluator.setInboundTransform(transform);
+
+        verify(mIpSecPacketLossDetector).setInboundTransform(transform);
+    }
+
+    @Test
+    public void testSetIpSecTransform_onUnSelectedNetwork() throws Exception {
+        mNetworkEvaluator.setIsSelected(
+                false,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        mNetworkEvaluator.setInboundTransform(makeDummyIpSecTransform());
+
+        verify(mIpSecPacketLossDetector, never()).setInboundTransform(any());
+    }
+
+    @Test
+    public void close() throws Exception {
+        mNetworkEvaluator.close();
+
+        verify(mIpSecPacketLossDetector).close();
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    private NetworkMetricMonitorCallback getMetricMonitorCbCaptor() throws Exception {
+        verify(mDependencies)
+                .newIpSecPacketLossDetector(any(), any(), any(), mMetricMonitorCbCaptor.capture());
+
+        return mMetricMonitorCbCaptor.getValue();
+    }
+
+    private void checkPenalizeNetwork() throws Exception {
+        assertFalse(mNetworkEvaluator.isPenalized());
+
+        // Validation failed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verify the evaluator is penalized
+        assertTrue(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_penaltyTimeout() throws Exception {
+        checkPenalizeNetwork();
+
+        // Penalty timeout
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        // Verify the evaluator is not penalized
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_passValidation() throws Exception {
+        checkPenalizeNetwork();
+
+        // Validation passed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verify the evaluator is not penalized and penalty timeout is canceled
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged();
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_closeEvaluator() throws Exception {
+        checkPenalizeNetwork();
+
+        mNetworkEvaluator.close();
+
+        // Verify penalty timeout is canceled
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testRcvValidationResult_PenaltyStateUnchanged() throws Exception {
+        assertFalse(mNetworkEvaluator.isPenalized());
+
+        // Validation passed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verifications
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, never()).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testSetCarrierConfig() throws Exception {
+        final int additionalTimeoutMin = 10;
+        when(mCarrierConfig.getIntArray(
+                        eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject()))
+                .thenReturn(new int[] {PENALTY_TIMEOUT_MIN + additionalTimeoutMin});
+
+        // Update evaluator and penalize the network
+        mNetworkEvaluator.reevaluate(
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        checkPenalizeNetwork();
+
+        // Verify penalty timeout is changed
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(TimeUnit.MINUTES.toMillis(additionalTimeoutMin));
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Verify NetworkMetricMonitor is notified
+        verify(mIpSecPacketLossDetector).setCarrierConfig(any());
+    }
+
+    @Test
+    public void testCompare() throws Exception {
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        final UnderlyingNetworkEvaluator penalized = mNetworkEvaluator;
+        final UnderlyingNetworkEvaluator notPenalized = newValidUnderlyingNetworkEvaluator();
+
+        assertEquals(penalized.getPriorityClass(), notPenalized.getPriorityClass());
+
+        final int result =
+                UnderlyingNetworkEvaluator.getComparator(mVcnContext)
+                        .compare(penalized, notPenalized);
+        assertEquals(1, result);
+    }
 }