Merge "Camera: Fix doc typo" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index e6c2b3b..0212fe3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -294,7 +294,7 @@
     field public static final String SET_ALARM = "com.android.alarm.permission.SET_ALARM";
     field public static final String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH";
     field public static final String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE";
-    field @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public static final String SET_BIOMETRIC_DIALOG_ADVANCED = "android.permission.SET_BIOMETRIC_DIALOG_ADVANCED";
+    field public static final String SET_BIOMETRIC_DIALOG_ADVANCED = "android.permission.SET_BIOMETRIC_DIALOG_ADVANCED";
     field public static final String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP";
     field @Deprecated public static final String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS";
     field public static final String SET_PROCESS_LIMIT = "android.permission.SET_PROCESS_LIMIT";
@@ -16747,7 +16747,7 @@
     method public boolean hasGlyph(String);
     method public final boolean isAntiAlias();
     method public final boolean isDither();
-    method public boolean isElegantTextHeight();
+    method @Deprecated @FlaggedApi("com.android.text.flags.deprecate_elegant_text_height_api") public boolean isElegantTextHeight();
     method public final boolean isFakeBoldText();
     method public final boolean isFilterBitmap();
     method public final boolean isLinearText();
@@ -16768,7 +16768,7 @@
     method public void setColor(@ColorLong long);
     method public android.graphics.ColorFilter setColorFilter(android.graphics.ColorFilter);
     method public void setDither(boolean);
-    method public void setElegantTextHeight(boolean);
+    method @Deprecated @FlaggedApi("com.android.text.flags.deprecate_elegant_text_height_api") public void setElegantTextHeight(boolean);
     method public void setEndHyphenEdit(int);
     method public void setFakeBoldText(boolean);
     method public void setFilterBitmap(boolean);
@@ -17361,6 +17361,25 @@
     method public boolean setUseCompositingLayer(boolean, @Nullable android.graphics.Paint);
   }
 
+  @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeColorFilter extends android.graphics.ColorFilter {
+    ctor public RuntimeColorFilter(@NonNull String);
+    method public void setColorUniform(@NonNull String, @ColorInt int);
+    method public void setColorUniform(@NonNull String, @ColorLong long);
+    method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color);
+    method public void setFloatUniform(@NonNull String, float);
+    method public void setFloatUniform(@NonNull String, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float, float);
+    method public void setFloatUniform(@NonNull String, @NonNull float[]);
+    method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter);
+    method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader);
+    method public void setIntUniform(@NonNull String, int);
+    method public void setIntUniform(@NonNull String, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int, int);
+    method public void setIntUniform(@NonNull String, @NonNull int[]);
+  }
+
   public class RuntimeShader extends android.graphics.Shader {
     ctor public RuntimeShader(@NonNull String);
     method public void setColorUniform(@NonNull String, @ColorInt int);
@@ -19103,11 +19122,11 @@
     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 android.hardware.biometrics.PromptContentView getContentView();
     method @Nullable public CharSequence getDescription();
-    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap getLogoBitmap();
-    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getLogoDescription();
-    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
+    method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap getLogoBitmap();
+    method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getLogoDescription();
+    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
     method @Nullable public CharSequence getNegativeButtonText();
     method @Nullable public CharSequence getSubtitle();
     method @NonNull public CharSequence getTitle();
@@ -19156,12 +19175,12 @@
     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 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 @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap);
-    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String);
-    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap);
+    method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence);
@@ -19184,27 +19203,27 @@
     method @Nullable public java.security.Signature getSignature();
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentItem {
+  public interface PromptContentItem {
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
+  public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
     ctor public PromptContentItemBulletedText(@NonNull String);
     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.PromptContentItemBulletedText> CREATOR;
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
+  public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
     ctor public PromptContentItemPlainText(@NonNull String);
     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.PromptContentItemPlainText> CREATOR;
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView {
+  public interface PromptContentView {
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentViewWithMoreOptionsButton implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
+  public final class PromptContentViewWithMoreOptionsButton implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
     method public int describeContents();
     method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getDescription();
     method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.content.DialogInterface.OnClickListener getMoreOptionsButtonListener();
@@ -19219,7 +19238,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder setMoreOptionsButtonListener(@NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
   }
 
-  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
+  public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
     method public int describeContents();
     method @Nullable public String getDescription();
     method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems();
@@ -20109,6 +20128,7 @@
 
   public class MultiResolutionImageReader implements java.lang.AutoCloseable {
     ctor public MultiResolutionImageReader(@NonNull java.util.Collection<android.hardware.camera2.params.MultiResolutionStreamInfo>, int, @IntRange(from=1) int);
+    ctor @FlaggedApi("com.android.internal.camera.flags.multiresolution_imagereader_usage_public") public MultiResolutionImageReader(@NonNull java.util.Collection<android.hardware.camera2.params.MultiResolutionStreamInfo>, int, @IntRange(from=1) int, long);
     method public void close();
     method protected void finalize();
     method public void flush();
@@ -22668,7 +22688,6 @@
     method public void sendEvent(int, int, @Nullable byte[]) throws android.media.MediaCasException;
     method public void setEventListener(@Nullable android.media.MediaCas.EventListener, @Nullable android.os.Handler);
     method public void setPrivateData(@NonNull byte[]) throws android.media.MediaCasException;
-    method @FlaggedApi("com.android.media.flags.update_client_profile_priority") public boolean updateResourcePriority(int, int);
     field public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = 0; // 0x0
     field public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = 1; // 0x1
     field public static final int SCRAMBLING_MODE_AES128 = 9; // 0x9
@@ -34179,6 +34198,7 @@
     method public android.os.StrictMode.VmPolicy build();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectActivityLeaks();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectAll();
+    method @FlaggedApi("com.android.window.flags.bal_strict_mode_ro") @NonNull public android.os.StrictMode.VmPolicy.Builder detectBlockedBackgroundActivityLaunch();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCleartextNetwork();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectContentUriWithoutPermission();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCredentialProtectedWhileLocked();
@@ -34191,6 +34211,7 @@
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectNonSdkApiUsage();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUnsafeIntentLaunch();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUntaggedSockets();
+    method @FlaggedApi("com.android.window.flags.bal_strict_mode_ro") @NonNull public android.os.StrictMode.VmPolicy.Builder ignoreBlockedBackgroundActivityLaunch();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeath();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnCleartextNetwork();
     method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnFileUriExposure();
@@ -41479,6 +41500,7 @@
     method public final void cancelNotification(String);
     method public final void cancelNotifications(String[]);
     method public final void clearRequestedListenerHints();
+    method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @Nullable public final android.app.NotificationChannel createConversationNotificationChannelForPackage(@NonNull String, @NonNull android.os.UserHandle, @NonNull String, @NonNull String);
     method public android.service.notification.StatusBarNotification[] getActiveNotifications();
     method public android.service.notification.StatusBarNotification[] getActiveNotifications(String[]);
     method public final int getCurrentInterruptionFilter();
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 4d1a423..bc73220 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -102,6 +102,7 @@
     method @NonNull public android.os.UserHandle getUser();
     field public static final String PAC_PROXY_SERVICE = "pac_proxy";
     field public static final String TEST_NETWORK_SERVICE = "test_network";
+    field @FlaggedApi("android.os.mainline_vcn_platform_api") public static final String VCN_MANAGEMENT_SERVICE = "vcn_management";
     field @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static final String WEBVIEW_UPDATE_SERVICE = "webviewupdate";
   }
 
@@ -144,21 +145,12 @@
 
   public class UsbManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getGadgetHalVersion();
-    method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbBandwidthMbps();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbHalVersion();
     field public static final int GADGET_HAL_NOT_SUPPORTED = -1; // 0xffffffff
     field public static final int GADGET_HAL_V1_0 = 10; // 0xa
     field public static final int GADGET_HAL_V1_1 = 11; // 0xb
     field public static final int GADGET_HAL_V1_2 = 12; // 0xc
     field public static final int GADGET_HAL_V2_0 = 20; // 0x14
-    field public static final int USB_DATA_TRANSFER_RATE_10G = 10240; // 0x2800
-    field public static final int USB_DATA_TRANSFER_RATE_20G = 20480; // 0x5000
-    field public static final int USB_DATA_TRANSFER_RATE_40G = 40960; // 0xa000
-    field public static final int USB_DATA_TRANSFER_RATE_5G = 5120; // 0x1400
-    field public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12; // 0xc
-    field public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480; // 0x1e0
-    field public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2; // 0x2
-    field public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1; // 0xffffffff
     field public static final int USB_HAL_NOT_SUPPORTED = -1; // 0xffffffff
     field public static final int USB_HAL_RETRY = -2; // 0xfffffffe
     field public static final int USB_HAL_V1_0 = 10; // 0xa
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bba52f5..9879e7f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1047,6 +1047,7 @@
     method public int getUserLockedFields();
     method public boolean isDeleted();
     method public void populateFromXml(org.xmlpull.v1.XmlPullParser);
+    method @FlaggedApi("android.service.notification.notification_conversation_channel_management") public void setImportantConversation(boolean);
     method public org.json.JSONObject toJson() throws org.json.JSONException;
     method public void writeXml(org.xmlpull.v1.XmlSerializer) throws java.io.IOException;
     field public static final int USER_LOCKED_SOUND = 32; // 0x20
@@ -5286,11 +5287,13 @@
   public final class VirtualDisplayConfig implements android.os.Parcelable {
     method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @Nullable public android.view.DisplayCutout getDisplayCutout();
     method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") public boolean isHomeSupported();
+    method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") public boolean isIgnoreActivitySizeRestrictions();
   }
 
   public static final class VirtualDisplayConfig.Builder {
     method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCutout(@Nullable android.view.DisplayCutout);
     method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean);
+    method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setIgnoreActivitySizeRestrictions(boolean);
   }
 
 }
@@ -7066,6 +7069,7 @@
   public class UsbManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public long getCurrentFunctions();
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_USB) public java.util.List<android.hardware.usb.UsbPort> getPorts();
+    method @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbBandwidthMbps();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public void grantPermission(android.hardware.usb.UsbDevice, String);
     method public static boolean isUvcSupportEnabled();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public void registerDisplayPortAltModeInfoListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.usb.UsbManager.DisplayPortAltModeInfoListener);
@@ -7092,6 +7096,14 @@
     field public static final long FUNCTION_UVC = 128L; // 0x80L
     field public static final String USB_CONFIGURED = "configured";
     field public static final String USB_CONNECTED = "connected";
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_10G = 10240; // 0x2800
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_20G = 20480; // 0x5000
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_40G = 40960; // 0xa000
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_5G = 5120; // 0x1400
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12; // 0xc
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480; // 0x1e0
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2; // 0x2
+    field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1; // 0xffffffff
     field public static final String USB_FUNCTION_NCM = "ncm";
     field public static final String USB_FUNCTION_RNDIS = "rndis";
   }
@@ -7218,6 +7230,7 @@
     field @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.MODIFY_AUDIO_ROUTING}) public static final int USAGE_CALL_ASSISTANT = 17; // 0x11
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_EMERGENCY = 1000; // 0x3e8
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_SAFETY = 1001; // 0x3e9
+    field @FlaggedApi("android.media.audio.speaker_cleanup_usage") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_SPEAKER_CLEANUP = 1004; // 0x3ec
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_VEHICLE_STATUS = 1002; // 0x3ea
   }
 
@@ -7572,6 +7585,11 @@
     method @NonNull public android.media.HwAudioSource.Builder setAudioDeviceInfo(@NonNull android.media.AudioDeviceInfo);
   }
 
+  public final class MediaCas implements java.lang.AutoCloseable {
+    method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean);
+    method @FlaggedApi("com.android.media.flags.update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int);
+  }
+
   public final class MediaCodec {
     method @NonNull @RequiresPermission(android.Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID) public static android.media.MediaCodec createByCodecNameForClient(@NonNull String, int, int) throws java.io.IOException;
   }
@@ -8344,6 +8362,7 @@
     method public int setLnaEnabled(boolean);
     method public int setMaxNumberOfFrontends(int, @IntRange(from=0) int);
     method public void setOnTuneEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.frontend.OnTuneEventListener);
+    method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean);
     method public void setResourceLostListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.Tuner.OnResourceLostListener);
     method public void shareFrontendFromTuner(@NonNull android.media.tv.tuner.Tuner);
     method public int transferOwner(@NonNull android.media.tv.tuner.Tuner);
@@ -10991,7 +11010,7 @@
   public class Environment {
     method @NonNull public static java.io.File getDataCePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String);
     method @NonNull public static java.io.File getDataDePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String);
-    method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeDirectory();
+    method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeviceProtectedDirectory();
     method @NonNull public static java.util.Collection<java.io.File> getInternalMediaDirectories();
     method @NonNull public static java.io.File getOdmDirectory();
     method @NonNull public static java.io.File getOemDirectory();
@@ -15686,6 +15705,9 @@
     method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean getEmergencyCallbackMode();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion();
+    method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @NonNull @RequiresPermission(value=android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional=true) public java.util.List<java.lang.String> getImsPcscfAddresses();
+    method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @Nullable @RequiresPermission(android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER) public String getImsPrivateUserIdentity();
+    method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @NonNull @RequiresPermission(value=android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional=true) public java.util.List<android.net.Uri> getImsPublicUserIdentities();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst();
     method @FlaggedApi("com.android.server.telecom.flags.get_last_known_cell_identity") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity();
@@ -15704,6 +15726,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimCardState(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimCardState(int, int);
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Locale getSimLocale();
+    method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void getSimServiceTable(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<byte[],java.lang.Exception>);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Collection<android.telephony.UiccSlotMapping> getSimSlotMapping();
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<android.telephony.RadioAccessSpecifier> getSystemSelectionChannels();
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
@@ -16828,8 +16851,11 @@
     method public void callSessionRttMessageReceived(String);
     method public void callSessionRttModifyRequestReceived(android.telephony.ims.ImsCallProfile);
     method public void callSessionRttModifyResponseReceived(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void callSessionSendAnbrQuery(int, int, @IntRange(from=0) int);
     method public void callSessionSuppServiceReceived(android.telephony.ims.ImsSuppServiceNotification);
     method public void callSessionTerminated(android.telephony.ims.ImsReasonInfo);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public void callSessionTransferFailed(@NonNull android.telephony.ims.ImsReasonInfo);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public void callSessionTransferred();
     method public void callSessionTtyModeReceived(int);
     method public void callSessionUpdateFailed(android.telephony.ims.ImsReasonInfo);
     method public void callSessionUpdateReceived(android.telephony.ims.ImsCallProfile);
@@ -17634,6 +17660,26 @@
     method public int getRadioTech();
   }
 
+  @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final class ConnectionFailureInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getCauseCode();
+    method public int getReason();
+    method public int getWaitTimeMillis();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.feature.ConnectionFailureInfo> CREATOR;
+    field public static final int REASON_ACCESS_DENIED = 1; // 0x1
+    field public static final int REASON_NAS_FAILURE = 2; // 0x2
+    field public static final int REASON_NONE = 0; // 0x0
+    field public static final int REASON_NO_SERVICE = 7; // 0x7
+    field public static final int REASON_PDN_NOT_AVAILABLE = 8; // 0x8
+    field public static final int REASON_RACH_FAILURE = 3; // 0x3
+    field public static final int REASON_RF_BUSY = 9; // 0x9
+    field public static final int REASON_RLC_FAILURE = 4; // 0x4
+    field public static final int REASON_RRC_REJECT = 5; // 0x5
+    field public static final int REASON_RRC_TIMEOUT = 6; // 0x6
+    field public static final int REASON_UNSPECIFIED = 65535; // 0xffff
+  }
+
   public abstract class ImsFeature {
     ctor public ImsFeature();
     method public abstract void changeEnabledCapabilities(android.telephony.ims.feature.CapabilityChangeRequest, android.telephony.ims.feature.ImsFeature.CapabilityCallbackProxy);
@@ -17660,6 +17706,11 @@
     method public void onChangeCapabilityConfigurationError(int, int, int);
   }
 
+  @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public interface ImsTrafficSessionCallback {
+    method public void onError(@NonNull android.telephony.ims.feature.ConnectionFailureInfo);
+    method public void onReady();
+  }
+
   public class MmTelFeature extends android.telephony.ims.feature.ImsFeature {
     ctor public MmTelFeature();
     ctor public MmTelFeature(@NonNull java.util.concurrent.Executor);
@@ -17672,6 +17723,7 @@
     method @NonNull public android.telephony.ims.stub.ImsMultiEndpointImplBase getMultiEndpoint();
     method @NonNull public android.telephony.ims.stub.ImsSmsImplBase getSmsImplementation();
     method @NonNull public android.telephony.ims.stub.ImsUtImplBase getUt();
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void modifyImsTrafficSession(int, @NonNull android.telephony.ims.feature.ImsTrafficSessionCallback);
     method public final void notifyCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities);
     method @Deprecated public final void notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull android.os.Bundle);
     method @Nullable public final android.telephony.ims.ImsCallSessionListener notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull String, @NonNull android.os.Bundle);
@@ -17692,10 +17744,24 @@
     method public void setTerminalBasedCallWaitingStatus(boolean);
     method public void setUiTtyMode(int, @Nullable android.os.Message);
     method public int shouldProcessCall(@NonNull String[]);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void startImsTrafficSession(int, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.feature.ImsTrafficSessionCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void stopImsTrafficSession(@NonNull android.telephony.ims.feature.ImsTrafficSessionCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void triggerEpsFallback(int);
     field public static final int AUDIO_HANDLER_ANDROID = 0; // 0x0
     field public static final int AUDIO_HANDLER_BASEBAND = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1; // 0x1
     field public static final String EXTRA_IS_UNKNOWN_CALL = "android.telephony.ims.feature.extra.IS_UNKNOWN_CALL";
     field public static final String EXTRA_IS_USSD = "android.telephony.ims.feature.extra.IS_USSD";
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_DIRECTION_INCOMING = 0; // 0x0
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_DIRECTION_OUTGOING = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_EMERGENCY = 0; // 0x0
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_EMERGENCY_SMS = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_REGISTRATION = 5; // 0x5
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_SMS = 4; // 0x4
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_UT_XCAP = 6; // 0x6
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_VIDEO = 3; // 0x3
+    field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_VOICE = 2; // 0x2
     field public static final int PROCESS_CALL_CSFB = 1; // 0x1
     field public static final int PROCESS_CALL_IMS = 0; // 0x0
   }
@@ -18179,7 +18245,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteDatagramReceived(long, @NonNull android.telephony.satellite.SatelliteDatagram, int, @NonNull java.util.function.Consumer<java.lang.Void>);
   }
 
-  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteManager {
+  public final class SatelliteManager {
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(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 deprovisionService(@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.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a0c4fdb..98d6f58 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -369,6 +369,7 @@
   }
 
   public final class NotificationChannel implements android.os.Parcelable {
+    method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @NonNull public android.app.NotificationChannel copy();
     method public int getOriginalImportance();
     method public boolean isImportanceLockedByCriticalDeviceFunction();
     method public void lockFields(int);
@@ -376,7 +377,7 @@
     method public void setDeletedTimeMs(long);
     method public void setDemoted(boolean);
     method public void setImportanceLockedByCriticalDeviceFunction(boolean);
-    method public void setImportantConversation(boolean);
+    method @FlaggedApi("android.service.notification.notification_conversation_channel_management") public void setImportantConversation(boolean);
     method public void setOriginalImportance(int);
     method public void setUserVisibleTaskShown(boolean);
     field @FlaggedApi("android.service.notification.notification_classification") public static final String NEWS_ID = "android.app.news";
@@ -1029,6 +1030,7 @@
   public abstract class Context {
     method @NonNull public java.io.File getCrateDir(@NonNull String);
     method public abstract int getDisplayId();
+    method @NonNull public java.util.List<android.content.IntentFilter> getRegisteredIntentFilters(@NonNull android.content.BroadcastReceiver);
     method @NonNull public android.os.UserHandle getUser();
     method public int getUserId();
     method public void setAutofillOptions(@Nullable android.content.AutofillOptions);
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index ecef0db..2cf718e 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1962,6 +1962,24 @@
         }
     }
 
+    @Override
+    @NonNull
+    public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) {
+        if (mPackageInfo != null) {
+            IIntentReceiver rd = mPackageInfo.findRegisteredReceiverDispatcher(
+                    receiver, getOuterContext());
+            try {
+                final List<IntentFilter> filters = ActivityManager.getService()
+                        .getRegisteredIntentFilters(rd);
+                return filters == null ? new ArrayList<>() : filters;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            throw new RuntimeException("Not supported in system context");
+        }
+    }
+
     private void validateServiceIntent(Intent service) {
         if (service.getComponent() == null && service.getPackage() == null) {
             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index f880901..34a3ad1 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -181,6 +181,7 @@
             in IntentFilter filter, in String requiredPermission, int userId, int flags);
     @UnsupportedAppUsage
     void unregisterReceiver(in IIntentReceiver receiver);
+    List<IntentFilter> getRegisteredIntentFilters(in IIntentReceiver receiver);
     /** @deprecated Use {@link #broadcastIntentWithFeature} instead */
     @UnsupportedAppUsage(maxTargetSdk=29, publicAlternatives="Use {@link android.content.Context#sendBroadcast(android.content.Intent)} instead")
     int broadcastIntent(in IApplicationThread caller, in Intent intent,
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index 0a05144..003104a 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -359,6 +359,23 @@
             in RemoteCallback navigationObserver, in BackAnimationAdapter adaptor);
 
     /**
+     * registers a callback to be invoked when a background activity launch is aborted.
+     *
+     * @param observer callback to be registered.
+     * @return true if the callback was successfully registered, false otherwise.
+     * @hide
+     */
+    boolean registerBackgroundActivityStartCallback(in IBinder binder);
+
+    /**
+     * unregisters a callback to be invoked when a background activity launch is aborted.
+     *
+     * @param observer callback to be registered.
+     * @hide
+     */
+    void unregisterBackgroundActivityStartCallback(in IBinder binder);
+
+    /**
      * registers a callback to be invoked when the screen is captured.
      *
      * @param observer callback to be registered.
diff --git a/core/java/android/app/IBackgroundActivityLaunchCallback.aidl b/core/java/android/app/IBackgroundActivityLaunchCallback.aidl
new file mode 100644
index 0000000..6dfb518
--- /dev/null
+++ b/core/java/android/app/IBackgroundActivityLaunchCallback.aidl
@@ -0,0 +1,26 @@
+/*
+* Copyright 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 android.app;
+
+/**
+ * Callback to find out when a background activity launch is aborted.
+ * @hide
+ */
+oneway interface IBackgroundActivityLaunchCallback
+{
+    void onBackgroundActivityLaunchAborted(in String message);
+}
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index a7597b4..a97fa18 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -175,6 +175,7 @@
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
     void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser);
 
+    NotificationChannel createConversationNotificationChannelForPackageFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, String parentChannelId, String conversationId);
     void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group);
     void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel);
     ParceledListSlice getNotificationChannelsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user);
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 1e45d6f..b8233bc 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1627,6 +1627,18 @@
         }
     }
 
+    IIntentReceiver findRegisteredReceiverDispatcher(BroadcastReceiver r, Context context) {
+        synchronized (mReceivers) {
+            final ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map =
+                    mReceivers.get(context);
+            if (map != null) {
+                final LoadedApk.ReceiverDispatcher rd = map.get(r);
+                return rd == null ? null : rd.getIIntentReceiver();
+            }
+            return null;
+        }
+    }
+
     public IIntentReceiver forgetReceiverDispatcher(Context context,
             BroadcastReceiver r) {
         synchronized (mReceivers) {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index ca1662e6..c6c0395 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -11831,7 +11831,7 @@
                     if (length <= 0) continue;
 
                     try {
-                        totalLength += Math.addExact(totalLength, length);
+                        totalLength = Math.addExact(totalLength, length);
                         segments.add(sanitizeSegment(segment, backgroundColor,
                                 defaultProgressColor));
                     } catch (ArithmeticException e) {
@@ -11853,7 +11853,6 @@
                 for (Point point : mProgressPoints) {
                     final int position = point.getPosition();
                     if (position < 0 || position > totalLength) continue;
-
                     points.add(sanitizePoint(point, backgroundColor, defaultProgressColor));
                 }
 
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index ebe7b3a..73d26b8 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -15,6 +15,8 @@
  */
 package android.app;
 
+import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT;
+
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -475,9 +477,10 @@
         dest.writeBoolean(mImportanceLockedDefaultApp);
     }
 
-    /**
-     * @hide
-     */
+    /** @hide */
+    @TestApi
+    @NonNull
+    @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
     public NotificationChannel copy() {
         NotificationChannel copy = new NotificationChannel(mId, mName, mImportance);
         copy.setDescription(mDesc);
@@ -548,10 +551,10 @@
         mDeletedTime = time;
     }
 
-    /**
-     * @hide
-     */
+    /** @hide */
     @TestApi
+    @SystemApi
+    @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
     public void setImportantConversation(boolean importantConvo) {
         mImportantConvo = importantConvo;
     }
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 8944bb9..74cae07 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -50,29 +50,40 @@
  * <p>**Building App Functions:**
  *
  * <p>Most developers should build app functions through the AppFunctions SDK. This SDK library
- * offers a more convenient and type-safe way to represent the inputs and outputs of an app
- * function, using custom data classes called "AppFunction Schemas".
+ * offers a more convenient and type-safe way to build app functions. The SDK provides predefined
+ * function schemas for common use cases and associated data classes for function parameters and
+ * return values. Apps only have to implement the provided interfaces. Internally, the SDK converts
+ * these data classes into {@link ExecuteAppFunctionRequest#getParameters()} and
+ * {@link ExecuteAppFunctionResponse#getResultDocument()}.
  *
- * <p>The suggested way to build an app function is to use the AppFunctions SDK. The SDK provides
- * custom data classes (AppFunctions Schemas) and handles the conversion to the underlying {@link
- * android.app.appsearch.GenericDocument}/{@link android.os.Bundle} format used in {@link
- * ExecuteAppFunctionRequest} and {@link ExecuteAppFunctionResponse}.
- *
- * <p>**Discovering (Listing) App Functions:**
+ * <p>**Discovering App Functions:**
  *
  * <p>When there is a package change or the device starts up, the metadata of available functions is
- * indexed on-device by {@link AppSearchManager}. AppSearch stores the indexed information as a
- * {@code AppFunctionStaticMetadata} document. This allows other apps and the app itself to discover
- * these functions using the AppSearch search APIs. Visibility to this metadata document is based on
- * the packages that have visibility to the app providing the app functions.
+ * indexed on-device by {@link AppSearchManager}. AppSearch stores the indexed information as an
+ * {@code AppFunctionStaticMetadata} document. This document contains the {@code functionIdentifier}
+ * and the schema information that the app function implements. This allows other apps and the app
+ * itself to discover these functions using the AppSearch search APIs. Visibility to this metadata
+ * document is based on the packages that have visibility to the app providing the app functions.
+ * AppFunction SDK provides a convenient way to achieve this and is the preferred method.
  *
  * <p>**Executing App Functions:**
  *
- * <p>Requests to execute a function are built using the {@link ExecuteAppFunctionRequest} class.
- * Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS} or {@code
- * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} permission to execute app functions from other
- * apps. An app has automatic visibility to its own functions and doesn't need these permissions to
- * call its own functions via {@code AppFunctionManager}.
+ * <p>To execute an app function, the caller app can retrieve the {@code functionIdentifier} from
+ * the {@code AppFunctionStaticMetadata} document and use it to build an
+ * {@link ExecuteAppFunctionRequest}. Then, invoke {@link #executeAppFunction} with the request
+ * to execute the app function. Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS}
+ * or {@code  android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} permission to execute app
+ * functions from other apps. An app can always execute its own app functions and doesn't need these
+ * permissions. AppFunction SDK provides a convenient way to achieve this and is the preferred
+ * method.
+ *
+ * <p>**Example:**
+ *
+ * <p>An assistant app is trying to fulfill the user request "Save XYZ into my note". The assistant
+ * app should first list all available app functions as {@code AppFunctionStaticMetadata} documents
+ * from AppSearch. Then, it should identify an app function that implements the {@code CreateNote}
+ * schema. Finally, the assistant app can invoke {@link #executeAppFunction} with the {@code
+ * functionIdentifier} of the chosen function.
  */
 @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
 @SystemService(Context.APP_FUNCTION_SERVICE)
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
index 4c5e8c1..41bb622 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
@@ -114,18 +114,14 @@
      * <p>The bundle may have missing parameters. Developers are advised to implement defensive
      * handling measures.
      *
-     * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be
-     * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This
-     * metadata will contain enough information for the caller to resolve the required parameters
-     * either using information from the metadata itself or using the AppFunction SDK for function
-     * callers.
+     * @see AppFunctionManager on how to determine the expected parameters.
      */
     @NonNull
     public GenericDocument getParameters() {
         return mParameters.getValue();
     }
 
-    /** Returns the additional data relevant to this function execution. */
+    /** Returns the additional metadata for this function execution request. */
     @NonNull
     public Bundle getExtras() {
         return mExtras;
@@ -153,19 +149,31 @@
         @NonNull
         private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
 
+        /**
+         * Creates a new instance of this builder class.
+         *
+         * @param targetPackageName The package name of the target app providing the app function to
+         *     invoke.
+         * @param functionIdentifier The identifier used by the {@link AppFunctionService} from the
+         *     target app to uniquely identify the function to be invoked.
+         */
         public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
             mTargetPackageName = Objects.requireNonNull(targetPackageName);
             mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
         }
 
-        /** Sets the additional data relevant to this function execution. */
+        /** Sets the additional metadata for this function execution request. */
         @NonNull
         public Builder setExtras(@NonNull Bundle extras) {
             mExtras = Objects.requireNonNull(extras);
             return this;
         }
 
-        /** Sets the function parameters. */
+        /**
+         * Sets the function parameters.
+         *
+         * @see #ExecuteAppFunctionRequest#getParameters()
+         */
         @NonNull
         public Builder setParameters(@NonNull GenericDocument parameters) {
             Objects.requireNonNull(parameters);
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index c907ef1..83453a9 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -156,7 +156,7 @@
      * Returns a successful response.
      *
      * @param resultDocument The return value of the executed function.
-     * @param extras The additional metadata data relevant to this function execution response.
+     * @param extras The additional metadata for this function execution response.
      */
     @NonNull
     @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
@@ -174,7 +174,7 @@
      * Returns a failure response.
      *
      * @param resultCode The result code of the app function execution.
-     * @param extras The additional metadata data relevant to this function execution response.
+     * @param extras The additional metadata for this function execution response.
      * @param errorMessage The error message associated with the result, if any.
      */
     @NonNull
@@ -216,13 +216,15 @@
      *       // Do something with the returnValue
      *     }
      * </pre>
+     *
+     * @see AppFunctionManager on how to determine the expected function return.
      */
     @NonNull
     public GenericDocument getResultDocument() {
         return mResultDocumentWrapper.getValue();
     }
 
-    /** Returns the extras of the app function execution. */
+    /** Returns the additional metadata for this function execution response. */
     @NonNull
     public Bundle getExtras() {
         return mExtras;
diff --git a/core/java/android/app/backup/NotificationLoggingConstants.java b/core/java/android/app/backup/NotificationLoggingConstants.java
index add4562..3601e86 100644
--- a/core/java/android/app/backup/NotificationLoggingConstants.java
+++ b/core/java/android/app/backup/NotificationLoggingConstants.java
@@ -23,10 +23,56 @@
     // Key under which the payload blob is stored
     public static final String KEY_NOTIFICATIONS = "notifications";
 
+    /**
+     * Events for android.service.notification.ZenModeConfig - the configuration for all modes
+     * settings for a single user
+     */
     @BackupRestoreEventLogger.BackupRestoreDataType
     public static final String DATA_TYPE_ZEN_CONFIG = KEY_NOTIFICATIONS + ":zen_config";
+    /**
+     * Events for android.service.notification.ZenModeConfig.ZenRule - a single mode within a
+     * ZenModeConfig
+     */
     @BackupRestoreEventLogger.BackupRestoreDataType
     public static final String DATA_TYPE_ZEN_RULES = KEY_NOTIFICATIONS + ":zen_rules";
+    /**
+     * Events for globally stored notifications data that aren't stored in settings, like whether
+     * to hide silent notification in the status bar
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_NOTIF_GLOBAL = KEY_NOTIFICATIONS + ":global";
+    /**
+     * Events for package specific notification settings, including app and
+     * android.app.NotificationChannel level settings.
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_NOTIF_PACKAGES = KEY_NOTIFICATIONS + ":packages";
+    /**
+     * Events for approved ManagedServices (NotificationListenerServices,
+     * NotificationAssistantService, ConditionProviderService).
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_MANAGED_SERVICE_PRIMARY_APPROVED = KEY_NOTIFICATIONS +
+            ":managed_service_primary_approved";
+    /**
+     * Events for what types of notifications NotificationListenerServices cannot see
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_NLS_RESTRICTED = KEY_NOTIFICATIONS +
+            ":nls_restricted";
+    /**
+     * Events for ManagedServices that are approved because they have a different primary
+     * ManagedService (ConditionProviderService).
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_MANAGED_SERVICE_SECONDARY_APPROVED = KEY_NOTIFICATIONS +
+            ":managed_service_secondary_approved";
+    /**
+     * Events for individual snoozed notifications.
+     */
+    @BackupRestoreEventLogger.BackupRestoreDataType
+    public static final String DATA_TYPE_SNOOZED = KEY_NOTIFICATIONS + ":snoozed";
+
 
     @BackupRestoreEventLogger.BackupRestoreError
     public static final String ERROR_XML_PARSING = KEY_NOTIFICATIONS + ":invalid_xml_parsing";
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 9eb6d56..9af2016 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -145,3 +145,11 @@
     bug: "370657575"
     is_exported: true
 }
+
+flag {
+    namespace: "virtual_devices"
+    name: "default_device_camera_access_policy"
+    description: "API for default device camera access policy"
+    bug: "371173368"
+    is_exported: true
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 5893da3..1d26b77 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3658,6 +3658,27 @@
     public abstract void unregisterReceiver(BroadcastReceiver receiver);
 
     /**
+     * Returns the list of {@link IntentFilter} objects that have been registered for the given
+     * {@link BroadcastReceiver}.
+     *
+     * @param receiver The {@link BroadcastReceiver} whose registered intent filters
+     *                 should be retrieved.
+     *
+     * @return A list of registered intent filters, or an empty list if the given receiver is not
+     *         registered.
+     *
+     * @throws NullPointerException if the {@code receiver} is {@code null}.
+     *
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi") // TestApi
+    @TestApi
+    @NonNull
+    public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * Request that a given application service be started.  The Intent
      * should either contain the complete class name of a specific service
      * implementation to start, or a specific package name to target.  If the
@@ -4873,6 +4894,8 @@
      * @see android.net.vcn.VcnManager
      * @hide
      */
+    @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String VCN_MANAGEMENT_SERVICE = "vcn_management";
 
     /**
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 79fa6ea..23d17cb 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -843,6 +843,13 @@
         mBase.unregisterReceiver(receiver);
     }
 
+    /** @hide */
+    @Override
+    @NonNull
+    public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) {
+        return mBase.getRegisteredIntentFilters(receiver);
+    }
+
     @Override
     public @Nullable ComponentName startService(Intent service) {
         return mBase.startService(service);
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 9bec598..2fdf258 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -41,10 +41,7 @@
 import android.app.ActivityThread;
 import android.app.AppGlobals;
 import android.app.StatusBarManager;
-import android.app.compat.CompatChanges;
 import android.bluetooth.BluetoothDevice;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.Overridable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
@@ -673,12 +670,6 @@
 @android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class Intent implements Parcelable, Cloneable {
     private static final String TAG = "Intent";
-
-    /** @hide */
-    @ChangeId
-    @Overridable
-    public static final long ENABLE_PREVENT_INTENT_REDIRECT = 29076063L;
-
     private static final String ATTR_ACTION = "action";
     private static final String TAG_CATEGORIES = "categories";
     private static final String ATTR_CATEGORY = "category";
@@ -12255,7 +12246,7 @@
      * @hide
      */
     public void collectExtraIntentKeys() {
-        if (!isPreventIntentRedirectEnabled()) return;
+        if (!preventIntentRedirect()) return;
 
         if (mExtras != null && !mExtras.isEmpty()) {
             for (String key : mExtras.keySet()) {
@@ -12272,14 +12263,6 @@
         }
     }
 
-    /**
-     * @hide
-     */
-    public static boolean isPreventIntentRedirectEnabled() {
-        return preventIntentRedirect() && CompatChanges.isChangeEnabled(
-                ENABLE_PREVENT_INTENT_REDIRECT);
-    }
-
     /** @hide */
     public void checkCreatorToken() {
         if (mExtras == null) return;
@@ -12368,7 +12351,7 @@
             out.writeInt(0);
         }
 
-        if (isPreventIntentRedirectEnabled()) {
+        if (preventIntentRedirect()) {
             if (mCreatorTokenInfo == null) {
                 out.writeInt(0);
             } else {
@@ -12435,7 +12418,7 @@
             mOriginalIntent = new Intent(in);
         }
 
-        if (isPreventIntentRedirectEnabled()) {
+        if (preventIntentRedirect()) {
             if (in.readInt() != 0) {
                 mCreatorTokenInfo = new CreatorTokenInfo();
                 mCreatorTokenInfo.mCreatorToken = in.readStrongBinder();
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 35f9cff..528bde8 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -536,7 +536,7 @@
 
 flag {
   name: "ignore_restrictions_when_deleting_private_profile"
-  namespace: "multiuser"
+  namespace: "profile_experiences"
   description: "Ignore any user restrictions when deleting private profiles."
   bug: "350953833"
   metadata {
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 0af2f25..e98fc0c 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -74,3 +74,12 @@
     description: "Feature flag for passing a dimension to create an frro"
     bug: "369672322"
 }
+
+flag {
+    name: "rro_control_for_android_no_overlayable"
+    is_exported: true
+    namespace: "resource_manager"
+    description: "Allow enabling and disabling RROs targeting android package with no overlayable"
+    bug: "364035303"
+}
+
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index d77e628..75c7e26 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -241,7 +241,8 @@
                     NoPreloadHolder.DEBUG_SQL_STATEMENTS, NoPreloadHolder.DEBUG_SQL_TIME,
                     mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount);
         } catch (SQLiteCantOpenDatabaseException e) {
-            final StringBuilder message = new StringBuilder(e.getMessage())
+            final StringBuilder message = new StringBuilder("Cannot open database ")
+                    .append("[").append(e.getMessage()).append("]")
                     .append(" '").append(file).append("'")
                     .append(" with flags 0x")
                     .append(Integer.toHexString(mConfiguration.openFlags));
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index e3fdd26..3ff21d8 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -22,7 +22,6 @@
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 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_CUSTOM_BIOMETRIC_PROMPT;
 import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
 
@@ -200,7 +199,6 @@
          * @param logoRes A drawable resource of the logo that will be shown on the prompt.
          * @return This builder.
          */
-        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
         @NonNull
         public BiometricPrompt.Builder setLogoRes(@DrawableRes int logoRes) {
@@ -226,7 +224,6 @@
          * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
          * @return This builder.
          */
-        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
         @NonNull
         public BiometricPrompt.Builder setLogoBitmap(@NonNull Bitmap logoBitmap) {
@@ -250,7 +247,6 @@
          * @return This builder.
          * @throws IllegalArgumentException If logo description is null.
          */
-        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
         @NonNull
         public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) {
@@ -342,7 +338,6 @@
          * @param view The customized view information.
          * @return This builder.
          */
-        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @NonNull
         public BiometricPrompt.Builder setContentView(@NonNull PromptContentView view) {
             mPromptInfo.setContentView(view);
@@ -851,7 +846,6 @@
      *
      * @return The drawable resource of the logo, or 0 if the prompt has no logo resource set.
      */
-    @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
     @DrawableRes
     public int getLogoRes() {
@@ -864,7 +858,6 @@
      *
      * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set.
      */
-    @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
     @Nullable
     public Bitmap getLogoBitmap() {
@@ -879,7 +872,6 @@
      * @return The logo description of the prompt, or null if the prompt has no logo description
      * set.
      */
-    @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
     @Nullable
     public String getLogoDescription() {
@@ -939,7 +931,6 @@
      *
      * @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();
diff --git a/core/java/android/hardware/biometrics/PromptContentItem.java b/core/java/android/hardware/biometrics/PromptContentItem.java
index c47b37a..0c41782 100644
--- a/core/java/android/hardware/biometrics/PromptContentItem.java
+++ b/core/java/android/hardware/biometrics/PromptContentItem.java
@@ -16,14 +16,9 @@
 
 package android.hardware.biometrics;
 
-import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
-
-import android.annotation.FlaggedApi;
-
 /**
  * An item shown on {@link PromptContentView}.
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public interface PromptContentItem {
 }
 
diff --git a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
index 25e5cca..a026498 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
@@ -16,9 +16,6 @@
 
 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;
@@ -26,7 +23,6 @@
 /**
  * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptContentItemBulletedText implements PromptContentItemParcelable {
     private final String mText;
 
diff --git a/core/java/android/hardware/biometrics/PromptContentItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java
index 668912cf..1860aae 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemParcelable.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java
@@ -16,15 +16,11 @@
 
 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 PromptContentItem}.
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 sealed interface PromptContentItemParcelable extends PromptContentItem, Parcelable
         permits PromptContentItemPlainText, PromptContentItemBulletedText {
 }
diff --git a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
index 7919256..a5e6120 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
@@ -16,9 +16,6 @@
 
 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;
@@ -26,7 +23,6 @@
 /**
  * A list item with plain text shown on {@link PromptVerticalListContentView}.
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptContentItemPlainText implements PromptContentItemParcelable {
     private final String mText;
 
diff --git a/core/java/android/hardware/biometrics/PromptContentView.java b/core/java/android/hardware/biometrics/PromptContentView.java
index ff9313e..0836d72 100644
--- a/core/java/android/hardware/biometrics/PromptContentView.java
+++ b/core/java/android/hardware/biometrics/PromptContentView.java
@@ -16,13 +16,8 @@
 
 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
index b5982d4..6e03563 100644
--- a/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
+++ b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
@@ -16,15 +16,11 @@
 
 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, PromptContentViewWithMoreOptionsButton {
 }
diff --git a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
index 4b9d5ce..aa0ce58 100644
--- a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
+++ b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
@@ -17,10 +17,8 @@
 package android.hardware.biometrics;
 
 import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
-import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
 
 import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -61,7 +59,6 @@
  *     .build();
  * </pre>
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptContentViewWithMoreOptionsButton implements PromptContentViewParcelable {
     private static final String TAG = "PromptContentViewWithMoreOptionsButton";
     @VisibleForTesting
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index e23ffeb..3e304e4 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -217,7 +217,7 @@
      * Returns if the PromptContentViewWithMoreOptionsButton is set.
      */
     public boolean isContentViewMoreOptionsButtonUsed() {
-        return Flags.customBiometricPrompt() && mContentView != null
+        return mContentView != null
                 && mContentView instanceof PromptContentViewWithMoreOptionsButton;
     }
 
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
index 86006f8..2a521d1 100644
--- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -16,9 +16,6 @@
 
 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;
@@ -48,7 +45,6 @@
  *     .build();
  * </pre>
  */
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptVerticalListContentView implements PromptContentViewParcelable {
     private static final String TAG = "PromptVerticalListContentView";
     @VisibleForTesting
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 52a4898..2d29900 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -54,3 +54,10 @@
   description: "This flag is for API changes related to Identity Check"
   bug: "373424727"
 }
+
+flag {
+  name: "private_space_bp"
+  namespace: "biometrics_framework"
+  description: "Feature flag for biometric prompt improvements in private space"
+  bug: "365554098"
+}
diff --git a/core/java/android/hardware/camera2/MultiResolutionImageReader.java b/core/java/android/hardware/camera2/MultiResolutionImageReader.java
index 116928b..8ede7f3 100644
--- a/core/java/android/hardware/camera2/MultiResolutionImageReader.java
+++ b/core/java/android/hardware/camera2/MultiResolutionImageReader.java
@@ -224,9 +224,8 @@
      * @see
      * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap
      *
-     * @hide
      */
-    @FlaggedApi(Flags.FLAG_MULTIRESOLUTION_IMAGEREADER_USAGE_CONFIG)
+    @FlaggedApi(Flags.FLAG_MULTIRESOLUTION_IMAGEREADER_USAGE_PUBLIC)
     public MultiResolutionImageReader(
             @NonNull Collection<MultiResolutionStreamInfo> streams,
             @Format             int format,
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index b0ea92d..a81bcbc 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -580,7 +580,7 @@
             EVENT_FLAG_DISPLAY_CONNECTION_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface EventsMask {}
+    public @interface EventFlag {}
 
     /**
      * Event type for when a new display is added.
@@ -774,7 +774,7 @@
      * @param listener The listener to register.
      * @param handler The handler on which the listener should be invoked, or null
      * if the listener should be invoked on the calling thread's looper.
-     * @param eventsMask A bitmask of the event types for which this listener is subscribed.
+     * @param eventFlagsMask A bitmask of the event types for which this listener is subscribed.
      *
      * @see #EVENT_FLAG_DISPLAY_ADDED
      * @see #EVENT_FLAG_DISPLAY_CHANGED
@@ -786,8 +786,8 @@
      * @hide
      */
     public void registerDisplayListener(@NonNull DisplayListener listener,
-            @Nullable Handler handler, @EventsMask long eventsMask) {
-        mGlobal.registerDisplayListener(listener, handler, eventsMask,
+            @Nullable Handler handler, @EventFlag long eventFlagsMask) {
+        mGlobal.registerDisplayListener(listener, handler, eventFlagsMask,
                 ActivityThread.currentPackageName());
     }
 
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 6affd12..3c6841c 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -17,7 +17,7 @@
 package android.hardware.display;
 
 
-import static android.hardware.display.DisplayManager.EventsMask;
+import static android.hardware.display.DisplayManager.EventFlag;
 import static android.view.Display.HdrCapabilities.HdrType;
 
 import android.Manifest;
@@ -130,7 +130,7 @@
     private final IDisplayManager mDm;
 
     private DisplayManagerCallback mCallback;
-    private @EventsMask long mRegisteredEventsMask = 0;
+    private @EventFlag long mRegisteredEventFlagsMask = 0;
     private final CopyOnWriteArrayList<DisplayListenerDelegate> mDisplayListeners =
             new CopyOnWriteArrayList<>();
 
@@ -346,10 +346,11 @@
      * @param packageName of the calling package.
      */
     public void registerDisplayListener(@NonNull DisplayListener listener,
-            @Nullable Handler handler, @EventsMask long eventsMask, String packageName) {
+            @Nullable Handler handler, @EventFlag long eventFlagsMask,
+            String packageName) {
         Looper looper = getLooperForHandler(handler);
         Handler springBoard = new Handler(looper);
-        registerDisplayListener(listener, new HandlerExecutor(springBoard), eventsMask,
+        registerDisplayListener(listener, new HandlerExecutor(springBoard), eventFlagsMask,
                 packageName);
     }
 
@@ -358,32 +359,32 @@
      *
      * @param listener The listener that will be called when display changes occur.
      * @param executor Executor for the thread that will be receiving the callbacks. Cannot be null.
-     * @param eventsMask Mask of events to be listened to.
+     * @param eventFlagsMask Flag of events to be listened to.
      * @param packageName of the calling package.
      */
     public void registerDisplayListener(@NonNull DisplayListener listener,
-            @NonNull Executor executor, @EventsMask long eventsMask, String packageName) {
+            @NonNull Executor executor, @EventFlag long eventFlagsMask, String packageName) {
         if (listener == null) {
             throw new IllegalArgumentException("listener must not be null");
         }
 
-        if (eventsMask == 0) {
+        if (eventFlagsMask == 0) {
             throw new IllegalArgumentException("The set of events to listen to must not be empty.");
         }
 
         if (extraLogging()) {
             Slog.i(TAG, "Registering Display Listener: "
-                    + Long.toBinaryString(eventsMask) + ", packageName: " + packageName);
+                    + Long.toBinaryString(eventFlagsMask) + ", packageName: " + packageName);
         }
 
         synchronized (mLock) {
             int index = findDisplayListenerLocked(listener);
             if (index < 0) {
-                mDisplayListeners.add(new DisplayListenerDelegate(listener, executor, eventsMask,
-                        packageName));
+                mDisplayListeners.add(new DisplayListenerDelegate(listener, executor,
+                        eventFlagsMask, packageName));
                 registerCallbackIfNeededLocked();
             } else {
-                mDisplayListeners.get(index).setEventsMask(eventsMask);
+                mDisplayListeners.get(index).setEventFlagsMask(eventFlagsMask);
             }
             updateCallbackIfNeededLocked();
             maybeLogAllDisplayListeners();
@@ -455,12 +456,12 @@
         return -1;
     }
 
-    @EventsMask
-    private int calculateEventsMaskLocked() {
+    @EventFlag
+    private int calculateEventFlagsMaskLocked() {
         int mask = 0;
         final int numListeners = mDisplayListeners.size();
         for (int i = 0; i < numListeners; i++) {
-            mask |= mDisplayListeners.get(i).mEventsMask;
+            mask |= mDisplayListeners.get(i).mEventFlagsMask;
         }
         if (mDispatchNativeCallbacks) {
             mask |= DisplayManager.EVENT_FLAG_DISPLAY_ADDED
@@ -478,14 +479,14 @@
     }
 
     private void updateCallbackIfNeededLocked() {
-        int mask = calculateEventsMaskLocked();
+        int mask = calculateEventFlagsMaskLocked();
         if (DEBUG) {
-            Log.d(TAG, "Mask for listener: " + mask);
+            Log.d(TAG, "Flag for listener: " + mask);
         }
-        if (mask != mRegisteredEventsMask) {
+        if (mask != mRegisteredEventFlagsMask) {
             try {
                 mDm.registerCallbackWithEventMask(mCallback, mask);
-                mRegisteredEventsMask = mask;
+                mRegisteredEventFlagsMask = mask;
             } catch (RemoteException ex) {
                 throw ex.rethrowFromSystemServer();
             }
@@ -1276,7 +1277,7 @@
 
     private static final class DisplayListenerDelegate {
         public final DisplayListener mListener;
-        public volatile long mEventsMask;
+        public volatile long mEventFlagsMask;
 
         private final DisplayInfo mDisplayInfo = new DisplayInfo();
         private final Executor mExecutor;
@@ -1284,10 +1285,10 @@
         private final String mPackageName;
 
         DisplayListenerDelegate(DisplayListener listener, @NonNull Executor executor,
-                @EventsMask long eventsMask, String packageName) {
+                @EventFlag long eventFlag, String packageName) {
             mExecutor = executor;
             mListener = listener;
-            mEventsMask = eventsMask;
+            mEventFlagsMask = eventFlag;
             mPackageName = packageName;
         }
 
@@ -1309,16 +1310,16 @@
             mGenerationId.incrementAndGet();
         }
 
-        void setEventsMask(@EventsMask long newEventsMask) {
-            mEventsMask = newEventsMask;
+        void setEventFlagsMask(@EventFlag long newEventsFlag) {
+            mEventFlagsMask = newEventsFlag;
         }
 
-        private void handleDisplayEventInner(int displayId, @DisplayEvent int event,
+        private void handleDisplayEventInner(int displayId, @DisplayEvent int eventFlagsMask,
                 @Nullable DisplayInfo info, boolean forceUpdate) {
             if (extraLogging()) {
-                Slog.i(TAG, "DLD(" + eventToString(event)
+                Slog.i(TAG, "DLD(" + eventToString(eventFlagsMask)
                         + ", display=" + displayId
-                        + ", mEventsMask=" + Long.toBinaryString(mEventsMask)
+                        + ", mEventsFlagMask=" + Long.toBinaryString(mEventFlagsMask)
                         + ", mPackageName=" + mPackageName
                         + ", displayInfo=" + info
                         + ", listener=" + mListener.getClass() + ")");
@@ -1326,18 +1327,18 @@
             if (DEBUG) {
                 Trace.beginSection(
                         TextUtils.trimToSize(
-                                "DLD(" + eventToString(event)
+                                "DLD(" + eventToString(eventFlagsMask)
                                 + ", display=" + displayId
                                 + ", listener=" + mListener.getClass() + ")", 127));
             }
-            switch (event) {
+            switch (eventFlagsMask) {
                 case EVENT_DISPLAY_ADDED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) {
                         mListener.onDisplayAdded(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_CHANGED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) {
                         if (info != null && (forceUpdate || !info.equals(mDisplayInfo))) {
                             if (extraLogging()) {
                                 Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: "
@@ -1349,27 +1350,29 @@
                     }
                     break;
                 case EVENT_DISPLAY_BRIGHTNESS_CHANGED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) {
                         mListener.onDisplayChanged(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_REMOVED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) {
                         mListener.onDisplayRemoved(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) {
                         mListener.onDisplayChanged(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_CONNECTED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED)
+                            != 0) {
                         mListener.onDisplayConnected(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_DISCONNECTED:
-                    if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                    if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED)
+                            != 0) {
                         mListener.onDisplayDisconnected(displayId);
                     }
                     break;
@@ -1381,7 +1384,7 @@
 
         @Override
         public String toString() {
-            return "mask: {" + mEventsMask + "}, for " + mListener.getClass();
+            return "mEventFlagsMask: {" + mEventFlagsMask + "}, for " + mListener.getClass();
         }
     }
 
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index b0994e6..49944c7 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -60,6 +60,7 @@
     private final float mRequestedRefreshRate;
     private final boolean mIsHomeSupported;
     private final DisplayCutout mDisplayCutout;
+    private final boolean mIgnoreActivitySizeRestrictions;
 
     private VirtualDisplayConfig(
             @NonNull String name,
@@ -74,7 +75,8 @@
             @NonNull ArraySet<String> displayCategories,
             float requestedRefreshRate,
             boolean isHomeSupported,
-            @Nullable DisplayCutout displayCutout) {
+            @Nullable DisplayCutout displayCutout,
+            boolean ignoreActivitySizeRestrictions) {
         mName = name;
         mWidth = width;
         mHeight = height;
@@ -88,6 +90,7 @@
         mRequestedRefreshRate = requestedRefreshRate;
         mIsHomeSupported = isHomeSupported;
         mDisplayCutout = displayCutout;
+        mIgnoreActivitySizeRestrictions = ignoreActivitySizeRestrictions;
     }
 
     /**
@@ -193,6 +196,20 @@
     }
 
     /**
+     * Whether this virtual display ignores fixed orientation, aspect ratio and resizability
+     * of apps.
+     *
+     * @see Builder#setIgnoreActivitySizeRestrictions(boolean)
+     * @hide
+     */
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    @SystemApi
+    public boolean isIgnoreActivitySizeRestrictions() {
+        return mIgnoreActivitySizeRestrictions
+                && com.android.window.flags.Flags.vdmForceAppUniversalResizableApi();
+    }
+
+    /**
      * Returns the display categories.
      *
      * @see Builder#setDisplayCategories
@@ -227,6 +244,7 @@
         dest.writeFloat(mRequestedRefreshRate);
         dest.writeBoolean(mIsHomeSupported);
         DisplayCutout.ParcelableWrapper.writeCutoutToParcel(mDisplayCutout, dest, flags);
+        dest.writeBoolean(mIgnoreActivitySizeRestrictions);
     }
 
     @Override
@@ -253,6 +271,7 @@
                 && Objects.equals(mDisplayCategories, that.mDisplayCategories)
                 && mRequestedRefreshRate == that.mRequestedRefreshRate
                 && mIsHomeSupported == that.mIsHomeSupported
+                && mIgnoreActivitySizeRestrictions == that.mIgnoreActivitySizeRestrictions
                 && Objects.equals(mDisplayCutout, that.mDisplayCutout);
     }
 
@@ -261,7 +280,8 @@
         int hashCode = Objects.hash(
                 mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId,
                 mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories,
-                mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout);
+                mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout,
+                mIgnoreActivitySizeRestrictions);
         return hashCode;
     }
 
@@ -282,6 +302,7 @@
                 + " mRequestedRefreshRate=" + mRequestedRefreshRate
                 + " mIsHomeSupported=" + mIsHomeSupported
                 + " mDisplayCutout=" + mDisplayCutout
+                + " mIgnoreActivitySizeRestrictions=" + mIgnoreActivitySizeRestrictions
                 + ")";
     }
 
@@ -299,6 +320,7 @@
         mRequestedRefreshRate = in.readFloat();
         mIsHomeSupported = in.readBoolean();
         mDisplayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(in);
+        mIgnoreActivitySizeRestrictions = in.readBoolean();
     }
 
     @NonNull
@@ -332,6 +354,7 @@
         private float mRequestedRefreshRate = 0.0f;
         private boolean mIsHomeSupported = false;
         private DisplayCutout mDisplayCutout = null;
+        private boolean mIgnoreActivitySizeRestrictions = false;
 
         /**
          * Creates a new Builder.
@@ -506,6 +529,24 @@
         }
 
         /**
+         * Sets whether this display ignores fixed orientation, aspect ratio and resizability
+         * of apps.
+         *
+         * <p>Note: setting to {@code true} requires the display to have
+         * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED}. If this is false, this property
+         * is ignored.</p>
+         *
+         * @hide
+         */
+        @FlaggedApi(com.android.window.flags.Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+        @SystemApi
+        @NonNull
+        public Builder setIgnoreActivitySizeRestrictions(boolean enabled) {
+            mIgnoreActivitySizeRestrictions = enabled;
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDisplayConfig} instance.
          */
         @NonNull
@@ -523,7 +564,8 @@
                     mDisplayCategories,
                     mRequestedRefreshRate,
                     mIsHomeSupported,
-                    mDisplayCutout);
+                    mDisplayCutout,
+                    mIgnoreActivitySizeRestrictions);
         }
     }
 }
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 0f290d9..9d42b67 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -113,6 +113,10 @@
     public static final int KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS = 65;
     public static final int KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS = 66;
     public static final int KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS = 67;
+    public static final int KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW = 68;
+    public static final int KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW = 69;
+    public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 70;
+    public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -194,7 +198,11 @@
             KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS,
             KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS,
             KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS,
-            KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS
+            KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS,
+            KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
+            KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
+            KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+            KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
@@ -541,6 +549,14 @@
                 return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE;
             case KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
                 return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION;
+            case KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW:
+                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_LEFT_FREEFORM_WINDOW;
+            case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW:
+                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_RIGHT_FREEFORM_WINDOW;
+            case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW:
+                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MAXIMIZE_FREEFORM_WINDOW;
+            case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE:
+                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RESTORE_FREEFORM_WINDOW_SIZE;
             default:
                 return LOG_EVENT_UNSPECIFIED;
         }
@@ -749,6 +765,14 @@
                 return "KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS";
             case KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
                 return "KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS";
+            case KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW:
+                return "KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW";
+            case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW:
+                return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW";
+            case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW:
+                return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW";
+            case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE:
+                return "KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index 41f344a..92608d0 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -21,6 +21,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.LongDef;
 import android.annotation.NonNull;
@@ -37,6 +38,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.hardware.usb.flags.Flags;
 import android.hardware.usb.gadget.GadgetFunction;
 import android.hardware.usb.gadget.UsbSpeed;
 import android.os.Binder;
@@ -509,7 +511,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1;
 
     /**
@@ -517,7 +520,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2;
 
     /**
@@ -525,7 +529,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12;
 
     /**
@@ -533,7 +538,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480;
 
     /**
@@ -541,7 +547,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_5G = 5 * 1024;
 
     /**
@@ -549,7 +556,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_10G = 10 * 1024;
 
     /**
@@ -557,7 +565,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_20G = 20 * 1024;
 
     /**
@@ -565,7 +574,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     public static final int USB_DATA_TRANSFER_RATE_40G = 40 * 1024;
 
     /**
@@ -1292,7 +1302,8 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API)
+    @SystemApi
     @RequiresPermission(Manifest.permission.MANAGE_USB)
     public int getUsbBandwidthMbps() {
         int usbSpeed;
diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
index 40e5ffb..3b7a9e9 100644
--- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
@@ -24,3 +24,10 @@
     description: "Feature flag to enable interface name as a parameter for device filter"
     bug: "312828160"
 }
+
+flag {
+    name: "expose_usb_speed_system_api"
+    namespace: "usb"
+    description: "Feature flag to enable exposing usb speed system api"
+    bug: "373653182"
+}
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 5b30624..1adefe5 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -10,8 +10,26 @@
 }
 
 flag {
+     name: "mainline_vcn_module_api"
+     namespace: "vcn"
+     description: "Expose APIs from VCN for mainline migration"
+     is_exported: true
+     bug: "376339506"
+}
+
+flag {
     name: "safe_mode_timeout_config"
     namespace: "vcn"
     description: "Feature flag for adjustable safe mode timeout"
     bug: "317406085"
+}
+
+flag {
+    name: "fix_config_garbage_collection"
+    namespace: "vcn"
+    description: "Handle race condition in subscription change"
+    bug: "370862489"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
new file mode 100644
index 0000000..6297318
--- /dev/null
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -0,0 +1,2512 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Process;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import dalvik.annotation.optimization.NeverCompile;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}.  Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
+public final class MessageQueue {
+    private static final String TAG_L = "LegacyMessageQueue";
+    private static final String TAG_C = "ConcurrentMessageQueue";
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
+
+    // True if the message queue can be quit.
+    @UnsupportedAppUsage
+    private final boolean mQuitAllowed;
+
+    @UnsupportedAppUsage
+    @SuppressWarnings("unused")
+    private long mPtr; // used by native code
+
+    @UnsupportedAppUsage
+    Message mMessages;
+    private Message mLast;
+    @UnsupportedAppUsage
+    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
+    private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
+    private IdleHandler[] mPendingIdleHandlers;
+    private boolean mLegacyQuitting;
+
+    // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
+    private boolean mLegacyBlocked;
+
+    // Tracks the number of async message. We use this in enqueueMessage() to avoid searching the
+    // queue for async messages when inserting a message at the tail.
+    private int mLegacyAsyncMessageCount;
+
+    /*
+     * Select between two implementations of message queue. The legacy implementation is used
+     * by default as it provides maximum compatibility with applications and tests that
+     * reach into MessageQueue via the mMessages field. The concurrent implemmentation is used for
+     * system processes and provides a higher level of concurrency and higher enqueue throughput
+     * than the legacy implementation.
+     */
+    private static boolean sForceConcurrent = false;
+
+    @RavenwoodRedirect
+    private native static long nativeInit();
+    @RavenwoodRedirect
+    private native static void nativeDestroy(long ptr);
+    @UnsupportedAppUsage
+    @RavenwoodRedirect
+    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    @RavenwoodRedirect
+    private native static void nativeWake(long ptr);
+    @RavenwoodRedirect
+    private native static boolean nativeIsPolling(long ptr);
+    @RavenwoodRedirect
+    private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
+
+    MessageQueue(boolean quitAllowed) {
+        if (!sForceConcurrent) {
+            sForceConcurrent = Process.myUid() < Process.FIRST_APPLICATION_UID;
+        }
+        mQuitAllowed = quitAllowed;
+        mPtr = nativeInit();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Disposes of the underlying message queue.
+    // Must only be called on the looper thread or the finalizer.
+    private void dispose() {
+        if (mPtr != 0) {
+            nativeDestroy(mPtr);
+            mPtr = 0;
+        }
+    }
+
+    private static final class MatchDeliverableMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when <= when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchDeliverableMessages mMatchDeliverableMessages =
+            new MatchDeliverableMessages();
+    /**
+     * Returns true if the looper has no pending messages which are due to be processed.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is idle.
+     */
+    public boolean isIdle() {
+        if (sForceConcurrent) {
+            final long now = SystemClock.uptimeMillis();
+
+            if (stackHasMessages(null, 0, null, null, now, mMatchDeliverableMessages, false)) {
+                return false;
+            }
+
+            MessageNode msgNode = null;
+            MessageNode asyncMsgNode = null;
+
+            if (!mPriorityQueue.isEmpty()) {
+                try {
+                    msgNode = mPriorityQueue.first();
+                } catch (NoSuchElementException e) { }
+            }
+
+            if (!mAsyncPriorityQueue.isEmpty()) {
+                try {
+                    asyncMsgNode = mAsyncPriorityQueue.first();
+                } catch (NoSuchElementException e) { }
+            }
+
+            if ((msgNode != null && msgNode.getWhen() <= now)
+                    || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) {
+                return false;
+            }
+
+            return true;
+        } else {
+            synchronized (this) {
+                final long now = SystemClock.uptimeMillis();
+                return mMessages == null || now < mMessages.when;
+            }
+        }
+    }
+
+    /**
+     * Add a new {@link IdleHandler} to this message queue.  This may be
+     * removed automatically for you by returning false from
+     * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+     * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be added.
+     */
+    public void addIdleHandler(@NonNull IdleHandler handler) {
+        if (handler == null) {
+            throw new NullPointerException("Can't add a null IdleHandler");
+        }
+        if (sForceConcurrent) {
+            synchronized (mIdleHandlersLock) {
+                mIdleHandlers.add(handler);
+            }
+        } else {
+            synchronized (this) {
+                mIdleHandlers.add(handler);
+            }
+        }
+    }
+
+    /**
+     * Remove an {@link IdleHandler} from the queue that was previously added
+     * with {@link #addIdleHandler}.  If the given object is not currently
+     * in the idle list, nothing is done.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be removed.
+     */
+    public void removeIdleHandler(@NonNull IdleHandler handler) {
+        if (sForceConcurrent) {
+            synchronized (mIdleHandlersLock) {
+                mIdleHandlers.remove(handler);
+            }
+        } else {
+            synchronized (this) {
+                mIdleHandlers.remove(handler);
+            }
+        }
+    }
+
+    /**
+     * Returns whether this looper's thread is currently polling for more work to do.
+     * This is a good signal that the loop is still alive rather than being stuck
+     * handling a callback.  Note that this method is intrinsically racy, since the
+     * state of the loop can change before you get the result back.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is currently polling for events.
+     * @hide
+     */
+    public boolean isPolling() {
+        if (sForceConcurrent) {
+            // If the loop is quitting then it must not be idling.
+            // We can assume mPtr != 0 when sQuitting is false.
+            return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr);
+        } else {
+            synchronized (this) {
+                return isPollingLocked();
+            }
+        }
+    }
+
+    private boolean isPollingLocked() {
+        // If the loop is quitting then it must not be idling.
+        // We can assume mPtr != 0 when mLegacyQuitting is false.
+        return !mLegacyQuitting && nativeIsPolling(mPtr);
+    }
+
+    /**
+     * Adds a file descriptor listener to receive notification when file descriptor
+     * related events occur.
+     * <p>
+     * If the file descriptor has already been registered, the specified events
+     * and listener will replace any that were previously associated with it.
+     * It is not possible to set more than one listener per file descriptor.
+     * </p><p>
+     * It is important to always unregister the listener when the file descriptor
+     * is no longer of use.
+     * </p>
+     *
+     * @param fd The file descriptor for which a listener will be registered.
+     * @param events The set of events to receive: a combination of the
+     * {@link OnFileDescriptorEventListener#EVENT_INPUT},
+     * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and
+     * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks.  If the requested
+     * set of events is zero, then the listener is unregistered.
+     * @param listener The listener to invoke when file descriptor events occur.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #removeOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd,
+            @OnFileDescriptorEventListener.Events int events,
+            @NonNull OnFileDescriptorEventListener listener) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        if (sForceConcurrent) {
+            synchronized (mFileDescriptorRecordsLock) {
+                updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+            }
+        } else {
+            synchronized (this) {
+                updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+            }
+        }
+    }
+
+    /**
+     * Removes a file descriptor listener.
+     * <p>
+     * This method does nothing if no listener has been registered for the
+     * specified file descriptor.
+     * </p>
+     *
+     * @param fd The file descriptor whose listener will be unregistered.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #addOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (sForceConcurrent) {
+            synchronized (mFileDescriptorRecordsLock) {
+                updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+            }
+        } else {
+            synchronized (this) {
+                updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+            }
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events,
+            OnFileDescriptorEventListener listener) {
+        final int fdNum = fd.getInt$();
+
+        int index = -1;
+        FileDescriptorRecord record = null;
+        if (mFileDescriptorRecords != null) {
+            index = mFileDescriptorRecords.indexOfKey(fdNum);
+            if (index >= 0) {
+                record = mFileDescriptorRecords.valueAt(index);
+                if (record != null && record.mEvents == events) {
+                    return;
+                }
+            }
+        }
+
+        if (events != 0) {
+            events |= OnFileDescriptorEventListener.EVENT_ERROR;
+            if (record == null) {
+                if (mFileDescriptorRecords == null) {
+                    mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>();
+                }
+                record = new FileDescriptorRecord(fd, events, listener);
+                mFileDescriptorRecords.put(fdNum, record);
+            } else {
+                record.mListener = listener;
+                record.mEvents = events;
+                record.mSeq += 1;
+            }
+            nativeSetFileDescriptorEvents(mPtr, fdNum, events);
+        } else if (record != null) {
+            record.mEvents = 0;
+            mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
+        }
+    }
+
+    // Called from native code.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int dispatchEvents(int fd, int events) {
+        // Get the file descriptor record and any state that might change.
+        final FileDescriptorRecord record;
+        final int oldWatchedEvents;
+        final OnFileDescriptorEventListener listener;
+        final int seq;
+        if (sForceConcurrent) {
+            synchronized (mFileDescriptorRecordsLock) {
+                record = mFileDescriptorRecords.get(fd);
+                if (record == null) {
+                    return 0; // spurious, no listener registered
+                }
+
+                oldWatchedEvents = record.mEvents;
+                events &= oldWatchedEvents; // filter events based on current watched set
+                if (events == 0) {
+                    return oldWatchedEvents; // spurious, watched events changed
+                }
+
+                listener = record.mListener;
+                seq = record.mSeq;
+            }
+        } else {
+            synchronized (this) {
+                record = mFileDescriptorRecords.get(fd);
+                if (record == null) {
+                    return 0; // spurious, no listener registered
+                }
+
+                oldWatchedEvents = record.mEvents;
+                events &= oldWatchedEvents; // filter events based on current watched set
+                if (events == 0) {
+                    return oldWatchedEvents; // spurious, watched events changed
+                }
+
+                listener = record.mListener;
+                seq = record.mSeq;
+            }
+        }
+        // Invoke the listener outside of the lock.
+        int newWatchedEvents = listener.onFileDescriptorEvents(
+                record.mDescriptor, events);
+        if (newWatchedEvents != 0) {
+            newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR;
+        }
+
+        // Update the file descriptor record if the listener changed the set of
+        // events to watch and the listener itself hasn't been updated since.
+        if (newWatchedEvents != oldWatchedEvents) {
+            synchronized (this) {
+                int index = mFileDescriptorRecords.indexOfKey(fd);
+                if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record
+                        && record.mSeq == seq) {
+                    record.mEvents = newWatchedEvents;
+                    if (newWatchedEvents == 0) {
+                        mFileDescriptorRecords.removeAt(index);
+                    }
+                }
+            }
+        }
+
+        // Return the new set of events to watch for native code to take care of.
+        return newWatchedEvents;
+    }
+
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
+    /* This is only read/written from the Looper thread. For use with Concurrent MQ */
+    private int mNextPollTimeoutMillis;
+    private boolean mMessageDirectlyQueued;
+    private Message nextMessage() {
+        int i = 0;
+
+        while (true) {
+            if (DEBUG) {
+                Log.d(TAG_C, "nextMessage loop #" + i);
+                i++;
+            }
+
+            mDrainingLock.lock();
+            mNextIsDrainingStack = true;
+            mDrainingLock.unlock();
+
+            /*
+             * Set our state to active, drain any items from the stack into our priority queues
+             */
+            StackNode oldTop;
+            oldTop = swapAndSetStackStateActive();
+            drainStack(oldTop);
+
+            mDrainingLock.lock();
+            mNextIsDrainingStack = false;
+            mDrainCompleted.signalAll();
+            mDrainingLock.unlock();
+
+            /*
+             * The objective of this next block of code is to:
+             *  - find a message to return (if any is ready)
+             *  - find a next message we would like to return, after scheduling.
+             *     - we make our scheduling decision based on this next message (if it exists).
+             *
+             * We have two queues to juggle and the presence of barriers throws an additional
+             * wrench into our plans.
+             *
+             * The last wrinkle is that remove() may delete items from underneath us. If we hit
+             * that case, we simply restart the loop.
+             */
+
+            /* Get the first node from each queue */
+            Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+            MessageNode msgNode = iterateNext(queueIter);
+            Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+            MessageNode asyncMsgNode = iterateNext(asyncQueueIter);
+
+            if (DEBUG) {
+                if (msgNode != null) {
+                    Message msg = msgNode.mMessage;
+                    Log.d(TAG_C, "Next found node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + msgNode.mInsertSeq + "barrier: "
+                            + msgNode.isBarrier() + " now: " + SystemClock.uptimeMillis());
+                }
+                if (asyncMsgNode != null) {
+                    Message msg = asyncMsgNode.mMessage;
+                    Log.d(TAG_C, "Next found async node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + asyncMsgNode.mInsertSeq + "barrier: "
+                            + asyncMsgNode.isBarrier() + " now: "
+                            + SystemClock.uptimeMillis());
+                }
+            }
+
+            /*
+             * the node which we will return, null if none are ready
+             */
+            MessageNode found = null;
+            /*
+             * The node from which we will determine our next wakeup time.
+             * Null indicates there is no next message ready. If we found a node,
+             * we can leave this null as Looper will call us again after delivering
+             * the message.
+             */
+            MessageNode next = null;
+
+            long now = SystemClock.uptimeMillis();
+            /*
+             * If we have a barrier we should return the async node (if it exists and is ready)
+             */
+            if (msgNode != null && msgNode.isBarrier()) {
+                if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) {
+                    found = asyncMsgNode;
+                } else {
+                    next = asyncMsgNode;
+                }
+            } else { /* No barrier. */
+                MessageNode earliest;
+                /*
+                 * If we have two messages, pick the earliest option from either queue.
+                 * Otherwise grab whichever node is non-null. If both are null we'll fall through.
+                 */
+                earliest = pickEarliestNode(msgNode, asyncMsgNode);
+
+                if (earliest != null) {
+                    if (now >= earliest.getWhen()) {
+                        found = earliest;
+                    } else {
+                        next = earliest;
+                    }
+                }
+            }
+
+            if (DEBUG) {
+                if (found != null) {
+                    Message msg = found.mMessage;
+                    Log.d(TAG_C, " Will deliver node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + found.mInsertSeq + " barrier: " + found.isBarrier()
+                            + " async: " + found.isAsync() + " now: "
+                            + SystemClock.uptimeMillis());
+                } else {
+                    Log.d(TAG_C, "No node to deliver");
+                }
+                if (next != null) {
+                    Message msg = next.mMessage;
+                    Log.d(TAG_C, "Next node what: " + msg.what + " when: " + msg.when + " seq: "
+                            + next.mInsertSeq + " barrier: " + next.isBarrier() + " async: "
+                            + next.isAsync()
+                            + " now: " + SystemClock.uptimeMillis());
+                } else {
+                    Log.d(TAG_C, "No next node");
+                }
+            }
+
+            /*
+             * If we have a found message, we will get called again so there's no need to set state.
+             * In that case we can leave our state as ACTIVE.
+             *
+             * Otherwise we should determine how to park the thread.
+             */
+            StateNode nextOp = sStackStateActive;
+            if (found == null) {
+                if (next == null) {
+                    /* No message to deliver, sleep indefinitely */
+                    mNextPollTimeoutMillis = -1;
+                    nextOp = sStackStateParked;
+                    if (DEBUG) {
+                        Log.d(TAG_C, "nextMessage next state is StackStateParked");
+                    }
+                } else {
+                    /* Message not ready, or we found one to deliver already, set a timeout */
+                    long nextMessageWhen = next.getWhen();
+                    if (nextMessageWhen > now) {
+                        mNextPollTimeoutMillis = (int) Math.min(nextMessageWhen - now,
+                                Integer.MAX_VALUE);
+                    } else {
+                        mNextPollTimeoutMillis = 0;
+                    }
+
+                    mStackStateTimedPark.mWhenToWake = now + mNextPollTimeoutMillis;
+                    nextOp = mStackStateTimedPark;
+                    if (DEBUG) {
+                        Log.d(TAG_C, "nextMessage next state is StackStateTimedParked timeout ms "
+                                + mNextPollTimeoutMillis + " mWhenToWake: "
+                                + mStackStateTimedPark.mWhenToWake + " now " + now);
+                    }
+                }
+            }
+
+            /*
+             * Try to swap our state from Active back to Park or TimedPark. If we raced with
+             * enqueue, loop back around to pick up any new items.
+             */
+            if (sState.compareAndSet(this, sStackStateActive, nextOp)) {
+                mMessageCounts.clearCounts();
+                if (found != null) {
+                    if (!removeFromPriorityQueue(found)) {
+                        /*
+                         * RemoveMessages() might be able to pull messages out from under us
+                         * However we can detect that here and just loop around if it happens.
+                         */
+                        continue;
+                    }
+
+                    if (TRACE) {
+                        Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                    }
+                    return found.mMessage;
+                }
+                return null;
+            }
+        }
+    }
+
+    private Message nextConcurrent() {
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        mNextPollTimeoutMillis = 0;
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        while (true) {
+            if (mNextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            mMessageDirectlyQueued = false;
+            nativePollOnce(ptr, mNextPollTimeoutMillis);
+
+            Message msg = nextMessage();
+            if (msg != null) {
+                msg.markInUse();
+                return msg;
+            }
+
+            if ((boolean) sQuitting.getVolatile(this)) {
+                return null;
+            }
+
+            synchronized (mIdleHandlersLock) {
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                if (pendingIdleHandlerCount < 0
+                        && isIdle()) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG_C, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (mIdleHandlersLock) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            mNextPollTimeoutMillis = 0;
+        }
+    }
+
+    @UnsupportedAppUsage
+    Message next() {
+        if (sForceConcurrent) {
+            return nextConcurrent();
+        }
+
+        // Return here if the message loop has already quit and been disposed.
+        // This can happen if the application tries to restart a looper after quit
+        // which is not supported.
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        int nextPollTimeoutMillis = 0;
+        for (;;) {
+            if (nextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            nativePollOnce(ptr, nextPollTimeoutMillis);
+
+            synchronized (this) {
+                // Try to retrieve the next message.  Return if found.
+                final long now = SystemClock.uptimeMillis();
+                Message prevMsg = null;
+                Message msg = mMessages;
+                if (msg != null && msg.target == null) {
+                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
+                    do {
+                        prevMsg = msg;
+                        msg = msg.next;
+                    } while (msg != null && !msg.isAsynchronous());
+                }
+                if (msg != null) {
+                    if (now < msg.when) {
+                        // Next message is not ready.  Set a timeout to wake up when it is ready.
+                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
+                    } else {
+                        // Got a message.
+                        mLegacyBlocked = false;
+                        if (prevMsg != null) {
+                            prevMsg.next = msg.next;
+                            if (prevMsg.next == null) {
+                                mLast = prevMsg;
+                            }
+                        } else {
+                            mMessages = msg.next;
+                            if (msg.next == null) {
+                                mLast = null;
+                            }
+                        }
+                        msg.next = null;
+                        if (DEBUG) Log.v(TAG_L, "Returning message: " + msg);
+                        msg.markInUse();
+                        if (msg.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        if (TRACE) {
+                            Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                        }
+                        return msg;
+                    }
+                } else {
+                    // No more messages.
+                    nextPollTimeoutMillis = -1;
+                }
+
+                // Process the quit message now that all pending messages have been handled.
+                if (mLegacyQuitting) {
+                    dispose();
+                    return null;
+                }
+
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                if (pendingIdleHandlerCount < 0
+                        && (mMessages == null || now < mMessages.when)) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    mLegacyBlocked = true;
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG_L, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (this) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            nextPollTimeoutMillis = 0;
+        }
+    }
+
+    void quit(boolean safe) {
+        if (!mQuitAllowed) {
+            throw new IllegalStateException("Main thread not allowed to quit.");
+        }
+
+        if (sForceConcurrent) {
+            synchronized (mIdleHandlersLock) {
+                if (sQuitting.compareAndSet(this, false, true)) {
+                    if (safe) {
+                        removeAllFutureMessages();
+                    } else {
+                        removeAllMessages();
+                    }
+
+                    // We can assume mPtr != 0 because sQuitting was previously false.
+                    nativeWake(mPtr);
+                }
+            }
+        } else {
+            synchronized (this) {
+                if (mLegacyQuitting) {
+                    return;
+                }
+                mLegacyQuitting = true;
+
+                if (safe) {
+                    removeAllFutureMessagesLocked();
+                } else {
+                    removeAllMessagesLocked();
+                }
+
+                // We can assume mPtr != 0 because mLegacyQuitting was previously false.
+                nativeWake(mPtr);
+            }
+        }
+    }
+
+    /**
+     * Posts a synchronization barrier to the Looper's message queue.
+     *
+     * Message processing occurs as usual until the message queue encounters the
+     * synchronization barrier that has been posted.  When the barrier is encountered,
+     * later synchronous messages in the queue are stalled (prevented from being executed)
+     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
+     * the token that identifies the synchronization barrier.
+     *
+     * This method is used to immediately postpone execution of all subsequently posted
+     * synchronous messages until a condition is met that releases the barrier.
+     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
+     * and continue to be processed as usual.
+     *
+     * This call must be always matched by a call to {@link #removeSyncBarrier} with
+     * the same token to ensure that the message queue resumes normal operation.
+     * Otherwise the application will probably hang!
+     *
+     * @return A token that uniquely identifies the barrier.  This token must be
+     * passed to {@link #removeSyncBarrier} to release the barrier.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public int postSyncBarrier() {
+        return postSyncBarrier(SystemClock.uptimeMillis());
+    }
+
+    private int postSyncBarrier(long when) {
+        // Enqueue a new sync barrier token.
+        // We don't need to wake the queue because the purpose of a barrier is to stall it.
+        if (sForceConcurrent) {
+            final int token = mNextBarrierTokenAtomic.getAndIncrement();
+
+            // b/376573804: apps and tests may expect to be able to use reflection
+            // to read this value. Make some effort to support this legacy use case.
+            mNextBarrierToken = token + 1;
+
+            final Message msg = Message.obtain();
+
+            msg.markInUse();
+            msg.arg1 = token;
+
+            if (!enqueueMessageUnchecked(msg, when)) {
+                Log.wtf(TAG_C, "Unexpected error while adding sync barrier!");
+                return -1;
+            }
+
+            return token;
+        }
+
+        synchronized (this) {
+            final int token = mNextBarrierToken++;
+            final Message msg = Message.obtain();
+            msg.markInUse();
+            msg.when = when;
+            msg.arg1 = token;
+
+            if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) {
+                /* Message goes to tail of list */
+                mLast.next = msg;
+                mLast = msg;
+                msg.next = null;
+                return token;
+            }
+
+            Message prev = null;
+            Message p = mMessages;
+            if (when != 0) {
+                while (p != null && p.when <= when) {
+                    prev = p;
+                    p = p.next;
+                }
+            }
+
+            if (p == null) {
+                /* We reached the tail of the list, or list is empty. */
+                mLast = msg;
+            }
+
+            if (prev != null) { // invariant: p == prev.next
+                msg.next = p;
+                prev.next = msg;
+            } else {
+                msg.next = p;
+                mMessages = msg;
+            }
+            return token;
+        }
+    }
+
+    private static final class MatchBarrierToken extends MessageCompare {
+        int mBarrierToken;
+
+        MatchBarrierToken(int token) {
+            super();
+            mBarrierToken = token;
+        }
+
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == null && m.arg1 == mBarrierToken) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Removes a synchronization barrier.
+     *
+     * @param token The synchronization barrier token that was returned by
+     * {@link #postSyncBarrier}.
+     *
+     * @throws IllegalStateException if the barrier was not found.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public void removeSyncBarrier(int token) {
+        // Remove a sync barrier token from the queue.
+        // If the queue is no longer stalled by a barrier then wake it.
+        if (sForceConcurrent) {
+            boolean removed;
+            MessageNode first;
+            final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token);
+
+            try {
+                /* Retain the first element to see if we are currently stuck on a barrier. */
+                first = mPriorityQueue.first();
+            } catch (NoSuchElementException e) {
+                /* The queue is empty */
+                first = null;
+            }
+
+            removed = findOrRemoveMessages(null, 0, null, null, 0, matchBarrierToken, true);
+            if (removed && first != null) {
+                Message m = first.mMessage;
+                if (m.target == null && m.arg1 == token) {
+                    /* Wake up next() in case it was sleeping on this barrier. */
+                    nativeWake(mPtr);
+                }
+            } else if (!removed) {
+                throw new IllegalStateException("The specified message queue synchronization "
+                        + " barrier token has not been posted or has already been removed.");
+            }
+            return;
+        }
+
+        synchronized (this) {
+            Message prev = null;
+            Message p = mMessages;
+            while (p != null && (p.target != null || p.arg1 != token)) {
+                prev = p;
+                p = p.next;
+            }
+            if (p == null) {
+                throw new IllegalStateException("The specified message queue synchronization "
+                        + " barrier token has not been posted or has already been removed.");
+            }
+            final boolean needWake;
+            if (prev != null) {
+                prev.next = p.next;
+                if (prev.next == null) {
+                    mLast = prev;
+                }
+                needWake = false;
+            } else {
+                mMessages = p.next;
+                if (mMessages == null) {
+                    mLast = null;
+                }
+                needWake = mMessages == null || mMessages.target != null;
+            }
+            p.recycleUnchecked();
+
+            // If the loop is quitting then it is already awake.
+            // We can assume mPtr != 0 when mLegacyQuitting is false.
+            if (needWake && !mLegacyQuitting) {
+                nativeWake(mPtr);
+            }
+        }
+    }
+
+    boolean enqueueMessage(Message msg, long when) {
+        if (msg.target == null) {
+            throw new IllegalArgumentException("Message must have a target.");
+        }
+
+        if (sForceConcurrent) {
+            if (msg.isInUse()) {
+                throw new IllegalStateException(msg + " This message is already in use.");
+            }
+
+            return enqueueMessageUnchecked(msg, when);
+        }
+
+        synchronized (this) {
+            if (msg.isInUse()) {
+                throw new IllegalStateException(msg + " This message is already in use.");
+            }
+
+            if (mLegacyQuitting) {
+                IllegalStateException e = new IllegalStateException(
+                        msg.target + " sending message to a Handler on a dead thread");
+                Log.w(TAG_L, e.getMessage(), e);
+                msg.recycle();
+                return false;
+            }
+
+            msg.markInUse();
+            msg.when = when;
+            Message p = mMessages;
+            boolean needWake;
+            if (p == null || when == 0 || when < p.when) {
+                // New head, wake up the event queue if blocked.
+                msg.next = p;
+                mMessages = msg;
+                needWake = mLegacyBlocked;
+                if (p == null) {
+                    mLast = mMessages;
+                }
+            } else {
+                // Message is to be inserted at tail or middle of queue. Usually we don't have to
+                // wake up the event queue unless there is a barrier at the head of the queue and
+                // the message is the earliest asynchronous message in the queue.
+                needWake = mLegacyBlocked && p.target == null && msg.isAsynchronous();
+
+                // For readability, we split this portion of the function into two blocks based on
+                // whether tail tracking is enabled. This has a minor implication for the case
+                // where tail tracking is disabled. See the comment below.
+                if (Flags.messageQueueTailTracking()) {
+                    if (when >= mLast.when) {
+                        needWake = needWake && mLegacyAsyncMessageCount == 0;
+                        msg.next = null;
+                        mLast.next = msg;
+                        mLast = msg;
+                    } else {
+                        // Inserted within the middle of the queue.
+                        Message prev;
+                        for (;;) {
+                            prev = p;
+                            p = p.next;
+                            if (p == null || when < p.when) {
+                                break;
+                            }
+                            if (needWake && p.isAsynchronous()) {
+                                needWake = false;
+                            }
+                        }
+                        if (p == null) {
+                            /* Inserting at tail of queue */
+                            mLast = msg;
+                        }
+                        msg.next = p; // invariant: p == prev.next
+                        prev.next = msg;
+                    }
+                } else {
+                    Message prev;
+                    for (;;) {
+                        prev = p;
+                        p = p.next;
+                        if (p == null || when < p.when) {
+                            break;
+                        }
+                        if (needWake && p.isAsynchronous()) {
+                            needWake = false;
+                        }
+                    }
+                    msg.next = p; // invariant: p == prev.next
+                    prev.next = msg;
+
+                    /*
+                     * If this block is executing then we have a build without tail tracking -
+                     * specifically: Flags.messageQueueTailTracking() == false. This is determined
+                     * at build time so the flag won't change on us during runtime.
+                     *
+                     * Since we don't want to pepper the code with extra checks, we only check
+                     * for tail tracking when we might use mLast. Otherwise, we continue to update
+                     * mLast as the tail of the list.
+                     *
+                     * In this case however we are not maintaining mLast correctly. Since we never
+                     * use it, this is fine. However, we run the risk of leaking a reference.
+                     * So set mLast to null in this case to avoid any Message leaks. The other
+                     * sites will never use the value so we are safe against null pointer derefs.
+                     */
+                    mLast = null;
+                }
+            }
+
+            if (msg.isAsynchronous()) {
+                mLegacyAsyncMessageCount++;
+            }
+
+            // We can assume mPtr != 0 because mLegacyQuitting is false.
+            if (needWake) {
+                nativeWake(mPtr);
+            }
+        }
+        return true;
+    }
+
+    private static final class MatchHandlerWhatAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject =
+            new MatchHandlerWhatAndObject();
+    boolean hasMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+        if (sForceConcurrent) {
+            return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject,
+                    false);
+        }
+        synchronized (this) {
+            Message p = mMessages;
+            while (p != null) {
+                if (p.target == h && p.what == what && (object == null || p.obj == object)) {
+                    return true;
+                }
+                p = p.next;
+            }
+            return false;
+        }
+    }
+
+    private static final class MatchHandlerWhatAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals =
+            new MatchHandlerWhatAndObjectEquals();
+    boolean hasEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+        if (sForceConcurrent) {
+            return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals,
+                    false);
+
+        }
+        synchronized (this) {
+            Message p = mMessages;
+            while (p != null) {
+                if (p.target == h && p.what == what && (object == null || object.equals(p.obj))) {
+                    return true;
+                }
+                p = p.next;
+            }
+            return false;
+        }
+    }
+
+    private static final class MatchHandlerRunnableAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject =
+            new MatchHandlerRunnableAndObject();
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    boolean hasMessages(Handler h, Runnable r, Object object) {
+        if (h == null) {
+            return false;
+        }
+        if (sForceConcurrent) {
+            return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject,
+                    false);
+        }
+
+        synchronized (this) {
+            Message p = mMessages;
+            while (p != null) {
+                if (p.target == h && p.callback == r && (object == null || p.obj == object)) {
+                    return true;
+                }
+                p = p.next;
+            }
+            return false;
+        }
+    }
+
+    private static final class MatchHandler extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandler mMatchHandler = new MatchHandler();
+    boolean hasMessages(Handler h) {
+        if (h == null) {
+            return false;
+        }
+        if (sForceConcurrent) {
+            return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false);
+        }
+        synchronized (this) {
+            Message p = mMessages;
+            while (p != null) {
+                if (p.target == h) {
+                    return true;
+                }
+                p = p.next;
+            }
+            return false;
+        }
+    }
+
+    void removeMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true);
+            return;
+        }
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h && p.what == what
+                   && (object == null || p.obj == object)) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && n.what == what
+                            && (object == null || n.obj == object)) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    void removeEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true);
+            return;
+        }
+
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h && p.what == what
+                   && (object == null || object.equals(p.obj))) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && n.what == what
+                            && (object == null || object.equals(n.obj))) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    void removeMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true);
+            return;
+        }
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h && p.callback == r
+                   && (object == null || p.obj == object)) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && n.callback == r
+                            && (object == null || n.obj == object)) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    private static final class MatchHandlerRunnableAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals =
+            new MatchHandlerRunnableAndObjectEquals();
+    void removeEqualMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true);
+            return;
+        }
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h && p.callback == r
+                   && (object == null || object.equals(p.obj))) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && n.callback == r
+                            && (object == null || object.equals(n.obj))) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    private static final class MatchHandlerAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject();
+    void removeCallbacksAndMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true);
+            return;
+        }
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h
+                    && (object == null || p.obj == object)) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && (object == null || n.obj == object)) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    private static final class MatchHandlerAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals =
+            new MatchHandlerAndObjectEquals();
+    void removeCallbacksAndEqualMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        if (sForceConcurrent) {
+            findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true);
+            return;
+        }
+        synchronized (this) {
+            Message p = mMessages;
+
+            // Remove all messages at front.
+            while (p != null && p.target == h
+                    && (object == null || object.equals(p.obj))) {
+                Message n = p.next;
+                mMessages = n;
+                if (p.isAsynchronous()) {
+                    mLegacyAsyncMessageCount--;
+                }
+                p.recycleUnchecked();
+                p = n;
+            }
+
+            if (p == null) {
+                mLast = mMessages;
+            }
+
+            // Remove all messages after front.
+            while (p != null) {
+                Message n = p.next;
+                if (n != null) {
+                    if (n.target == h && (object == null || object.equals(n.obj))) {
+                        Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mLegacyAsyncMessageCount--;
+                        }
+                        n.recycleUnchecked();
+                        p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
+                        continue;
+                    }
+                }
+                p = n;
+            }
+        }
+    }
+
+    private void removeAllMessagesLocked() {
+        Message p = mMessages;
+        while (p != null) {
+            Message n = p.next;
+            p.recycleUnchecked();
+            p = n;
+        }
+        mMessages = null;
+        mLast = null;
+        mLegacyAsyncMessageCount = 0;
+    }
+
+    private void removeAllFutureMessagesLocked() {
+        final long now = SystemClock.uptimeMillis();
+        Message p = mMessages;
+        if (p != null) {
+            if (p.when > now) {
+                removeAllMessagesLocked();
+            } else {
+                Message n;
+                for (;;) {
+                    n = p.next;
+                    if (n == null) {
+                        return;
+                    }
+                    if (n.when > now) {
+                        break;
+                    }
+                    p = n;
+                }
+                p.next = null;
+                mLast = p;
+
+                do {
+                    p = n;
+                    n = p.next;
+                    if (p.isAsynchronous()) {
+                        mLegacyAsyncMessageCount--;
+                    }
+                    p.recycleUnchecked();
+                } while (n != null);
+            }
+        }
+    }
+
+    private static final class MatchAllMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            return true;
+        }
+    }
+    private final MatchAllMessages mMatchAllMessages = new MatchAllMessages();
+    private void removeAllMessages() {
+        findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true);
+    }
+
+    private static final class MatchAllFutureMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when > when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchAllFutureMessages mMatchAllFutureMessages = new MatchAllFutureMessages();
+    private void removeAllFutureMessages() {
+        findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(),
+                mMatchAllFutureMessages, true);
+    }
+
+    @NeverCompile
+    private void printPriorityQueueNodes() {
+        Iterator<MessageNode> iterator = mPriorityQueue.iterator();
+
+        Log.d(TAG_C, "* Dump priority queue");
+        while (iterator.hasNext()) {
+            MessageNode msgNode = iterator.next();
+            Log.d(TAG_C, "** MessageNode what: " + msgNode.mMessage.what + " when "
+                    + msgNode.mMessage.when + " seq: " + msgNode.mInsertSeq);
+        }
+    }
+
+    @NeverCompile
+    private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue, Printer pw,
+            String prefix, Handler h, int n) {
+        int count = 0;
+        long now = SystemClock.uptimeMillis();
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            if (h == null || h == msg.target) {
+                pw.println(prefix + "Message " + (n + count) + ": " + msg.toString(now));
+            }
+            count++;
+        }
+        return count;
+    }
+
+    @NeverCompile
+    void dump(Printer pw, String prefix, Handler h) {
+        if (sForceConcurrent) {
+            long now = SystemClock.uptimeMillis();
+            int n = 0;
+
+            pw.println(prefix + "(MessageQueue is using Concurrent implementation)");
+
+            StackNode node = (StackNode) sState.getVolatile(this);
+            while (node != null) {
+                if (node.isMessageNode()) {
+                    Message msg = ((MessageNode) node).mMessage;
+                    if (h == null || h == msg.target) {
+                        pw.println(prefix + "Message " + n + ": " + msg.toString(now));
+                    }
+                    node = ((MessageNode) node).mNext;
+                } else {
+                    pw.println(prefix + "State: " + node);
+                    node = null;
+                }
+                n++;
+            }
+
+            pw.println(prefix + "PriorityQueue Messages: ");
+            n += dumpPriorityQueue(mPriorityQueue, pw, prefix, h, n);
+            pw.println(prefix + "AsyncPriorityQueue Messages: ");
+            n += dumpPriorityQueue(mAsyncPriorityQueue, pw, prefix, h, n);
+
+            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPolling()
+                    + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")");
+            return;
+        }
+
+        synchronized (this) {
+            pw.println(prefix + "(MessageQueue is using Legacy implementation)");
+            long now = SystemClock.uptimeMillis();
+            int n = 0;
+            for (Message msg = mMessages; msg != null; msg = msg.next) {
+                if (h == null || h == msg.target) {
+                    pw.println(prefix + "Message " + n + ": " + msg.toString(now));
+                }
+                n++;
+            }
+            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+                    + ", quitting=" + mLegacyQuitting + ")");
+        }
+    }
+
+    @NeverCompile
+    private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue,
+            ProtoOutputStream proto) {
+        int count = 0;
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            count++;
+        }
+        return count;
+    }
+
+    @NeverCompile
+    void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        if (sForceConcurrent) {
+            final long messageQueueToken = proto.start(fieldId);
+
+            StackNode node = (StackNode) sState.getVolatile(this);
+            while (node.isMessageNode()) {
+                Message msg = ((MessageNode) node).mMessage;
+                msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+                node = ((MessageNode) node).mNext;
+            }
+
+            dumpPriorityQueue(mPriorityQueue, proto);
+            dumpPriorityQueue(mAsyncPriorityQueue, proto);
+
+            proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPolling());
+            proto.write(MessageQueueProto.IS_QUITTING, (boolean) sQuitting.getVolatile(this));
+            proto.end(messageQueueToken);
+            return;
+        }
+
+        final long messageQueueToken = proto.start(fieldId);
+        synchronized (this) {
+            for (Message msg = mMessages; msg != null; msg = msg.next) {
+                msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            }
+            proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPollingLocked());
+            proto.write(MessageQueueProto.IS_QUITTING, mLegacyQuitting);
+        }
+        proto.end(messageQueueToken);
+    }
+
+    /**
+     * Callback interface for discovering when a thread is going to block
+     * waiting for more messages.
+     */
+    public static interface IdleHandler {
+        /**
+         * Called when the message queue has run out of messages and will now
+         * wait for more.  Return true to keep your idle handler active, false
+         * to have it removed.  This may be called if there are still messages
+         * pending in the queue, but they are all scheduled to be dispatched
+         * after the current time.
+         */
+        boolean queueIdle();
+    }
+
+    /**
+     * A listener which is invoked when file descriptor related events occur.
+     */
+    public interface OnFileDescriptorEventListener {
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for input
+         * operations, such as reading.
+         * <p>
+         * The listener should read all available data from the file descriptor
+         * then return <code>true</code> to keep the listener active or <code>false</code>
+         * to remove the listener.
+         * </p><p>
+         * In the case of a socket, this event may be generated to indicate
+         * that there is at least one incoming connection that the listener
+         * should accept.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_INPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_INPUT = 1 << 0;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for output
+         * operations, such as writing.
+         * <p>
+         * The listener should write as much data as it needs.  If it could not
+         * write everything at once, then it should return <code>true</code> to
+         * keep the listener active.  Otherwise, it should return <code>false</code>
+         * to remove the listener then re-register it later when it needs to write
+         * something else.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_OUTPUT = 1 << 1;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor encountered a
+         * fatal error.
+         * <p>
+         * File descriptor errors can occur for various reasons.  One common error
+         * is when the remote peer of a socket or pipe closes its end of the connection.
+         * </p><p>
+         * This event may be generated at any time regardless of whether the
+         * {@link #EVENT_ERROR} event mask was specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_ERROR = 1 << 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+                EVENT_INPUT,
+                EVENT_OUTPUT,
+                EVENT_ERROR
+        })
+        public @interface Events {}
+
+        /**
+         * Called when a file descriptor receives events.
+         *
+         * @param fd The file descriptor.
+         * @param events The set of events that occurred: a combination of the
+         * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks.
+         * @return The new set of events to watch, or 0 to unregister the listener.
+         *
+         * @see #EVENT_INPUT
+         * @see #EVENT_OUTPUT
+         * @see #EVENT_ERROR
+         */
+        @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events);
+    }
+
+    private static final class FileDescriptorRecord {
+        public final FileDescriptor mDescriptor;
+        public int mEvents;
+        public OnFileDescriptorEventListener mListener;
+        public int mSeq;
+
+        public FileDescriptorRecord(FileDescriptor descriptor,
+                int events, OnFileDescriptorEventListener listener) {
+            mDescriptor = descriptor;
+            mEvents = events;
+            mListener = listener;
+        }
+    }
+
+    /**
+     * ConcurrentMessageQueue specific classes methods and variables
+     */
+    /* Helper to choose the correct queue to insert into. */
+    private void insertIntoPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            mAsyncPriorityQueue.add(msgNode);
+        } else {
+            mPriorityQueue.add(msgNode);
+        }
+    }
+
+    private boolean removeFromPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            return mAsyncPriorityQueue.remove(msgNode);
+        } else {
+            return mPriorityQueue.remove(msgNode);
+        }
+    }
+
+    private MessageNode pickEarliestNode(MessageNode nodeA, MessageNode nodeB) {
+        if (nodeA != null && nodeB != null) {
+            if (nodeA.compareTo(nodeB) < 0) {
+                return nodeA;
+            }
+            return nodeB;
+        }
+
+        return nodeA != null ? nodeA : nodeB;
+    }
+
+    private MessageNode iterateNext(Iterator<MessageNode> iter) {
+        if (iter.hasNext()) {
+            try {
+                return iter.next();
+            } catch (NoSuchElementException e) {
+                /* The queue is empty - this can happen if we race with remove */
+            }
+        }
+        return null;
+    }
+
+    /* Move any non-cancelled messages into the priority queue */
+    private void drainStack(StackNode oldTop) {
+        while (oldTop.isMessageNode()) {
+            MessageNode oldTopMessageNode = (MessageNode) oldTop;
+            if (oldTopMessageNode.removeFromStack()) {
+                insertIntoPriorityQueue(oldTopMessageNode);
+            }
+            MessageNode inserted = oldTopMessageNode;
+            oldTop = oldTopMessageNode.mNext;
+            /*
+             * removeMessages can walk this list while we are consuming it.
+             * Set our next pointer to null *after* we add the message to our
+             * priority queue. This way removeMessages() will always find the
+             * message, either in our list or in the priority queue.
+             */
+            inserted.mNext = null;
+        }
+    }
+
+    /* Set the stack state to Active, return a list of nodes to walk. */
+    private StackNode swapAndSetStackStateActive() {
+        while (true) {
+            /* Set stack state to Active, get node list to walk later */
+            StackNode current = (StackNode) sState.getVolatile(this);
+            if (current == sStackStateActive
+                    || sState.compareAndSet(this, current, sStackStateActive)) {
+                return current;
+            }
+        }
+    }
+    private StateNode getStateNode(StackNode node) {
+        if (node.isMessageNode()) {
+            return ((MessageNode) node).mBottomOfStack;
+        }
+        return (StateNode) node;
+    }
+
+    private void waitForDrainCompleted() {
+        mDrainingLock.lock();
+        while (mNextIsDrainingStack) {
+            mDrainCompleted.awaitUninterruptibly();
+        }
+        mDrainingLock.unlock();
+    }
+
+    @IntDef(value = {
+        STACK_NODE_MESSAGE,
+        STACK_NODE_ACTIVE,
+        STACK_NODE_PARKED,
+        STACK_NODE_TIMEDPARK})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface StackNodeType {}
+
+    /*
+     * Stack node types. STACK_NODE_MESSAGE indicates a node containing a message.
+     * The other types indicate what state our Looper thread is in. The bottom of
+     * the stack is always a single state node. Message nodes are added on top.
+     */
+    private static final int STACK_NODE_MESSAGE = 0;
+    /*
+     * Active state indicates that next() is processing messages
+     */
+    private static final int STACK_NODE_ACTIVE = 1;
+    /*
+     * Parked state indicates that the Looper thread is sleeping indefinitely (nothing to deliver)
+     */
+    private static final int STACK_NODE_PARKED = 2;
+    /*
+     * Timed Park state indicates that the Looper thread is sleeping, waiting for a message
+     * deadline
+     */
+    private static final int STACK_NODE_TIMEDPARK = 3;
+
+    /* Describes a node in the Treiber stack */
+    static class StackNode {
+        @StackNodeType
+        private final int mType;
+
+        StackNode(@StackNodeType int type) {
+            mType = type;
+        }
+
+        @StackNodeType
+        final int getNodeType() {
+            return mType;
+        }
+
+        final boolean isMessageNode() {
+            return mType == STACK_NODE_MESSAGE;
+        }
+    }
+
+    static final class MessageNode extends StackNode implements Comparable<MessageNode> {
+        private final Message mMessage;
+        volatile StackNode mNext;
+        StateNode mBottomOfStack;
+        boolean mWokeUp;
+        final long mInsertSeq;
+        private static final VarHandle sRemovedFromStack;
+        private volatile boolean mRemovedFromStackValue;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                sRemovedFromStack = l.findVarHandle(MessageQueue.MessageNode.class,
+                        "mRemovedFromStackValue", boolean.class);
+            } catch (Exception e) {
+                Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e);
+                throw new ExceptionInInitializerError(e);
+            }
+        }
+
+        MessageNode(@NonNull Message message, long insertSeq) {
+            super(STACK_NODE_MESSAGE);
+            mMessage = message;
+            mInsertSeq = insertSeq;
+        }
+
+        long getWhen() {
+            return mMessage.when;
+        }
+
+        boolean isRemovedFromStack() {
+            return mRemovedFromStackValue;
+        }
+
+        boolean removeFromStack() {
+            return sRemovedFromStack.compareAndSet(this, false, true);
+        }
+
+        boolean isAsync() {
+            return mMessage.isAsynchronous();
+        }
+
+        boolean isBarrier() {
+            return mMessage.target == null;
+        }
+
+        @Override
+        public int compareTo(@NonNull MessageNode messageNode) {
+            Message other = messageNode.mMessage;
+
+            int compared = Long.compare(mMessage.when, other.when);
+            if (compared == 0) {
+                compared = Long.compare(mInsertSeq, messageNode.mInsertSeq);
+            }
+            return compared;
+        }
+    }
+
+    static class StateNode extends StackNode {
+        StateNode(int type) {
+            super(type);
+        }
+    }
+
+    static final class TimedParkStateNode extends StateNode {
+        long mWhenToWake;
+
+        TimedParkStateNode() {
+            super(STACK_NODE_TIMEDPARK);
+        }
+    }
+
+    private static final StateNode sStackStateActive = new StateNode(STACK_NODE_ACTIVE);
+    private static final StateNode sStackStateParked = new StateNode(STACK_NODE_PARKED);
+    private final TimedParkStateNode mStackStateTimedPark = new TimedParkStateNode();
+
+    /* This is the top of our treiber stack. */
+    private static final VarHandle sState;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sState = l.findVarHandle(MessageQueue.class, "mStateValue",
+                    MessageQueue.StackNode.class);
+        } catch (Exception e) {
+            Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    private volatile StackNode mStateValue = sStackStateParked;
+    private final ConcurrentSkipListSet<MessageNode> mPriorityQueue =
+            new ConcurrentSkipListSet<MessageNode>();
+    private final ConcurrentSkipListSet<MessageNode> mAsyncPriorityQueue =
+            new ConcurrentSkipListSet<MessageNode>();
+
+    /*
+     * This helps us ensure that messages with the same timestamp are inserted in FIFO order.
+     * Increments on each insert, starting at 0. MessageNode.compareTo() will compare sequences
+     * when delivery timestamps are identical.
+     */
+    private static final VarHandle sNextInsertSeq;
+    private volatile long mNextInsertSeqValue = 0;
+    /*
+     * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
+     * Those messages must be in LIFO order.
+     * Decrements on each front of queue insert.
+     */
+    private static final VarHandle sNextFrontInsertSeq;
+    private volatile long mNextFrontInsertSeqValue = -1;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sNextInsertSeq = l.findVarHandle(MessageQueue.class, "mNextInsertSeqValue",
+                    long.class);
+            sNextFrontInsertSeq = l.findVarHandle(MessageQueue.class, "mNextFrontInsertSeqValue",
+                    long.class);
+        } catch (Exception e) {
+            Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+
+    }
+
+    /*
+     * Tracks the number of queued and cancelled messages in our stack.
+     *
+     * On item cancellation, determine whether to wake next() to flush tombstoned messages.
+     * We track queued and cancelled counts as two ints packed into a single long.
+     */
+    private static final class MessageCounts {
+        private static VarHandle sCounts;
+        private volatile long mCountsValue = 0;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                sCounts = l.findVarHandle(MessageQueue.MessageCounts.class, "mCountsValue",
+                        long.class);
+            } catch (Exception e) {
+                Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e);
+                throw new ExceptionInInitializerError(e);
+            }
+        }
+
+        /* We use a special value to indicate when next() has been woken for flush. */
+        private static final long AWAKE = Long.MAX_VALUE;
+        /*
+         * Minimum number of messages in the stack which we need before we consider flushing
+         * tombstoned items.
+         */
+        private static final int MESSAGE_FLUSH_THRESHOLD = 10;
+
+        private static int numQueued(long val) {
+            return (int) (val >>> Integer.SIZE);
+        }
+
+        private static int numCancelled(long val) {
+            return (int) val;
+        }
+
+        private static long combineCounts(int queued, int cancelled) {
+            return ((long) queued << Integer.SIZE) | (long) cancelled;
+        }
+
+        public void incrementQueued() {
+            while (true) {
+                long oldVal = mCountsValue;
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                /* Use Math.max() to avoid overflow of queued count */
+                long newVal = combineCounts(Math.max(queued + 1, queued), cancelled);
+
+                /* Don't overwrite 'AWAKE' state */
+                if (oldVal == AWAKE || sCounts.compareAndSet(this, oldVal, newVal)) {
+                    break;
+                }
+            }
+        }
+
+        public boolean incrementCancelled() {
+            while (true) {
+                long oldVal = mCountsValue;
+                if (oldVal == AWAKE) {
+                    return false;
+                }
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                boolean needsPurge = queued > MESSAGE_FLUSH_THRESHOLD
+                        && (queued >> 1) < cancelled;
+                long newVal;
+                if (needsPurge) {
+                    newVal = AWAKE;
+                } else {
+                    newVal = combineCounts(queued,
+                            Math.max(cancelled + 1, cancelled));
+                }
+
+                if (sCounts.compareAndSet(this, oldVal, newVal)) {
+                    return needsPurge;
+                }
+            }
+        }
+
+        public void clearCounts() {
+            mCountsValue = 0;
+        }
+    }
+
+    private final MessageCounts mMessageCounts = new MessageCounts();
+
+    private final Object mIdleHandlersLock = new Object();
+    private final Object mFileDescriptorRecordsLock = new Object();
+
+    private static final VarHandle sQuitting;
+    private boolean mQuittingValue = false;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sQuitting = l.findVarHandle(MessageQueue.class, "mQuittingValue", boolean.class);
+        } catch (Exception e) {
+            Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    // The next barrier token.
+    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
+    private final AtomicInteger mNextBarrierTokenAtomic = new AtomicInteger(1);
+
+    // Must retain this for compatibility reasons.
+    @UnsupportedAppUsage
+    private int mNextBarrierToken;
+
+    /* Protects mNextIsDrainingStack */
+    private final ReentrantLock mDrainingLock = new ReentrantLock();
+    private boolean mNextIsDrainingStack = false;
+    private final Condition mDrainCompleted = mDrainingLock.newCondition();
+
+    private boolean enqueueMessageUnchecked(@NonNull Message msg, long when) {
+        if ((boolean) sQuitting.getVolatile(this)) {
+            IllegalStateException e = new IllegalStateException(
+                    msg.target + " sending message to a Handler on a dead thread");
+            Log.w(TAG_C, e.getMessage(), e);
+            msg.recycleUnchecked();
+            return false;
+        }
+
+        long seq = when != 0 ? ((long) sNextInsertSeq.getAndAdd(this, 1L) + 1L)
+                : ((long) sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L);
+        /* TODO: Add a MessageNode member to Message so we can avoid this allocation */
+        MessageNode node = new MessageNode(msg, seq);
+        msg.when = when;
+        msg.markInUse();
+
+        if (DEBUG) {
+            Log.d(TAG_C, "Insert message what: " + msg.what + " when: " + msg.when + " seq: "
+                    + node.mInsertSeq + " barrier: " + node.isBarrier() + " async: "
+                    + node.isAsync() + " now: " + SystemClock.uptimeMillis());
+        }
+
+        final Looper myLooper = Looper.myLooper();
+        /* If we are running on the looper thread we can add directly to the priority queue */
+        if (myLooper != null && myLooper.getQueue() == this) {
+            node.removeFromStack();
+            insertIntoPriorityQueue(node);
+            /*
+             * We still need to do this even though we are the current thread,
+             * otherwise next() may sleep indefinitely.
+             */
+            if (!mMessageDirectlyQueued) {
+                mMessageDirectlyQueued = true;
+                nativeWake(mPtr);
+            }
+            return true;
+        }
+
+        while (true) {
+            StackNode old = (StackNode) sState.getVolatile(this);
+            boolean wakeNeeded;
+            boolean inactive;
+
+            node.mNext = old;
+            switch (old.getNodeType()) {
+                case STACK_NODE_ACTIVE:
+                    /*
+                     * The worker thread is currently active and will process any elements added to
+                     * the stack before parking again.
+                     */
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = false;
+                    node.mWokeUp = true;
+                    wakeNeeded = false;
+                    break;
+
+                case STACK_NODE_PARKED:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    node.mWokeUp = true;
+                    wakeNeeded = true;
+                    break;
+
+                case STACK_NODE_TIMEDPARK:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    wakeNeeded = mStackStateTimedPark.mWhenToWake >= node.getWhen();
+                    node.mWokeUp = wakeNeeded;
+                    break;
+
+                default:
+                    MessageNode oldMessage = (MessageNode) old;
+
+                    node.mBottomOfStack = oldMessage.mBottomOfStack;
+                    int bottomType = node.mBottomOfStack.getNodeType();
+                    inactive = bottomType >= STACK_NODE_PARKED;
+                    wakeNeeded = (bottomType == STACK_NODE_TIMEDPARK
+                            && mStackStateTimedPark.mWhenToWake >= node.getWhen()
+                            && !oldMessage.mWokeUp);
+                    node.mWokeUp = oldMessage.mWokeUp || wakeNeeded;
+                    break;
+            }
+            if (sState.compareAndSet(this, old, node)) {
+                if (inactive) {
+                    if (wakeNeeded) {
+                        nativeWake(mPtr);
+                    } else {
+                        mMessageCounts.incrementQueued();
+                    }
+                }
+                return true;
+            }
+        }
+    }
+
+    /*
+     * This class is used to find matches for hasMessages() and removeMessages()
+     */
+    private abstract static class MessageCompare {
+        public abstract boolean compareMessage(Message m, Handler h, int what, Object object,
+                Runnable r, long when);
+    }
+
+    private boolean stackHasMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean found = false;
+        StackNode top = (StackNode) sState.getVolatile(this);
+        StateNode bottom = getStateNode(top);
+
+        /*
+         * If the top node is a state node, there are no reachable messages.
+         * If it's anything other than Active, we can quit as we know that next() is not
+         * consuming items.
+         * If the top node is Active then we know that next() is currently consuming items.
+         * In that case we should wait next() has drained the stack.
+         */
+        if (top == bottom) {
+            if (bottom != sStackStateActive) {
+                return false;
+            }
+            waitForDrainCompleted();
+            return false;
+        }
+
+        /*
+         * We have messages that we may tombstone. Walk the stack until we hit the bottom or we
+         * hit a null pointer.
+         * If we hit the bottom, we are done.
+         * If we hit a null pointer, then the stack is being consumed by next() and we must cycle
+         * until the stack has been drained.
+         */
+        MessageNode p = (MessageNode) top;
+
+        while (true) {
+            if (compare.compareMessage(p.mMessage, h, what, object, r, when)) {
+                found = true;
+                if (DEBUG) {
+                    Log.d(TAG_C, "stackHasMessages node matches");
+                }
+                if (removeMatches) {
+                    if (p.removeFromStack()) {
+                        p.mMessage.recycleUnchecked();
+                        if (mMessageCounts.incrementCancelled()) {
+                            nativeWake(mPtr);
+                        }
+                    }
+                } else {
+                    return true;
+                }
+            }
+
+            StackNode n = p.mNext;
+            if (n == null) {
+                /* Next() is walking the stack, we must re-sample */
+                if (DEBUG) {
+                    Log.d(TAG_C, "stackHasMessages next() is walking the stack, we must re-sample");
+                }
+                waitForDrainCompleted();
+                break;
+            }
+            if (!n.isMessageNode()) {
+                /* We reached the end of the stack */
+                return found;
+            }
+            p = (MessageNode) n;
+        }
+
+        return found;
+    }
+
+    private boolean priorityQueueHasMessage(ConcurrentSkipListSet<MessageNode> queue, Handler h,
+            int what, Object object, Runnable r, long when, MessageCompare compare,
+            boolean removeMatches) {
+        Iterator<MessageNode> iterator = queue.iterator();
+        boolean found = false;
+
+        while (iterator.hasNext()) {
+            MessageNode msg = iterator.next();
+
+            if (compare.compareMessage(msg.mMessage, h, what, object, r, when)) {
+                if (removeMatches) {
+                    found = true;
+                    if (queue.remove(msg)) {
+                        msg.mMessage.recycleUnchecked();
+                    }
+                } else {
+                    return true;
+                }
+            }
+        }
+        return found;
+    }
+
+    private boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean foundInStack, foundInQueue;
+
+        foundInStack = stackHasMessages(h, what, object, r, when, compare, removeMatches);
+        foundInQueue = priorityQueueHasMessage(mPriorityQueue, h, what, object, r, when, compare,
+                removeMatches);
+        foundInQueue |= priorityQueueHasMessage(mAsyncPriorityQueue, h, what, object, r, when,
+                compare, removeMatches);
+
+        return foundInStack || foundInQueue;
+    }
+
+}
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 8eaadde..9db88d1 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -382,6 +382,18 @@
         }
     }
 
+    private static final class MatchDeliverableMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when <= when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchDeliverableMessages mMatchDeliverableMessages =
+            new MatchDeliverableMessages();
     /**
      * Returns true if the looper has no pending messages which are due to be processed.
      *
@@ -390,6 +402,12 @@
      * @return True if the looper is idle.
      */
     public boolean isIdle() {
+        final long now = SystemClock.uptimeMillis();
+
+        if (stackHasMessages(null, 0, null, null, now, mMatchDeliverableMessages, false)) {
+            return false;
+        }
+
         MessageNode msgNode = null;
         MessageNode asyncMsgNode = null;
 
@@ -405,7 +423,6 @@
             } catch (NoSuchElementException e) { }
         }
 
-        final long now = SystemClock.uptimeMillis();
         if ((msgNode != null && msgNode.getWhen() <= now)
                 || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) {
             return false;
@@ -967,7 +984,7 @@
         return token;
     }
 
-    private class MatchBarrierToken extends MessageCompare {
+    private static final class MatchBarrierToken extends MessageCompare {
         int mBarrierToken;
 
         MatchBarrierToken(int token) {
@@ -1148,7 +1165,7 @@
         return foundInStack || foundInQueue;
     }
 
-    private static class MatchHandlerWhatAndObject extends MessageCompare {
+    private static final class MatchHandlerWhatAndObject extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1168,7 +1185,7 @@
         return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false);
     }
 
-    private static class MatchHandlerWhatAndObjectEquals extends MessageCompare {
+    private static final class MatchHandlerWhatAndObjectEquals extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1189,7 +1206,7 @@
                 false);
     }
 
-    private static class MatchHandlerRunnableAndObject extends MessageCompare {
+    private static final class MatchHandlerRunnableAndObject extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1210,7 +1227,7 @@
         return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false);
     }
 
-    private static class MatchHandler extends MessageCompare {
+    private static final class MatchHandler extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1249,7 +1266,7 @@
         findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true);
     }
 
-    private static class MatchHandlerRunnableAndObjectEquals extends MessageCompare {
+    private static final class MatchHandlerRunnableAndObjectEquals extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1268,7 +1285,7 @@
         findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true);
     }
 
-    private static class MatchHandlerAndObject extends MessageCompare {
+    private static final class MatchHandlerAndObject extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1286,7 +1303,7 @@
         findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true);
     }
 
-    private static class MatchHandlerAndObjectEquals extends MessageCompare {
+    private static final class MatchHandlerAndObjectEquals extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1305,7 +1322,7 @@
         findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true);
     }
 
-    private static class MatchAllMessages extends MessageCompare {
+    private static final class MatchAllMessages extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
@@ -1317,7 +1334,7 @@
         findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true);
     }
 
-    private static class MatchAllFutureMessages extends MessageCompare {
+    private static final class MatchAllFutureMessages extends MessageCompare {
         @Override
         public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
                 long when) {
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 89a5e5d..69540c42 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -417,6 +417,14 @@
      */
     @SystemApi
     @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY)
+    public static @NonNull File getDataSystemDeviceProtectedDirectory() {
+        return buildPath(getDataDirectory(), "system_de");
+    }
+
+    /** Use {@link #getDataSystemDeviceProtectedDirectory()} instead.
+     * {@hide}
+     */
+    @Deprecated
     public static @NonNull File getDataSystemDeDirectory() {
         return buildPath(getDataDirectory(), "system_de");
     }
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 81dc46e..b5ecc14 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -20,17 +20,22 @@
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH;
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH;
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH;
+import static com.android.window.flags.Flags.balStrictModeRo;
 
 import android.animation.ValueAnimator;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
 import android.app.ActivityThread;
 import android.app.IActivityManager;
+import android.app.IBackgroundActivityLaunchCallback;
 import android.app.IUnsafeIntentStrictModeCallback;
+import android.app.PendingIntent;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
@@ -45,6 +50,7 @@
 import android.net.TrafficStats;
 import android.net.Uri;
 import android.os.storage.IStorageManager;
+import android.os.strictmode.BackgroundActivityLaunchViolation;
 import android.os.strictmode.CleartextNetworkViolation;
 import android.os.strictmode.ContentUriWithoutPermissionViolation;
 import android.os.strictmode.CredentialProtectedWhileLockedViolation;
@@ -82,6 +88,7 @@
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.Preconditions;
+import com.android.window.flags.Flags;
 
 import dalvik.system.BlockGuard;
 import dalvik.system.CloseGuard;
@@ -266,6 +273,7 @@
             DETECT_VM_IMPLICIT_DIRECT_BOOT,
             DETECT_VM_INCORRECT_CONTEXT_USE,
             DETECT_VM_UNSAFE_INTENT_LAUNCH,
+            DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED,
             PENALTY_GATHER,
             PENALTY_LOG,
             PENALTY_DIALOG,
@@ -309,6 +317,8 @@
     private static final int DETECT_VM_INCORRECT_CONTEXT_USE = 1 << 12;
     /** @hide */
     private static final int DETECT_VM_UNSAFE_INTENT_LAUNCH = 1 << 13;
+    /** @hide */
+    private static final int DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED = 1 << 14;
 
     /** @hide */
     private static final int DETECT_VM_ALL = 0x0000ffff;
@@ -902,6 +912,9 @@
                 if (targetSdk >= Build.VERSION_CODES.S) {
                     detectUnsafeIntentLaunch();
                 }
+                if (balStrictModeRo() && targetSdk > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+                    detectBlockedBackgroundActivityLaunch();
+                }
 
                 // TODO: Decide whether to detect non SDK API usage beyond a certain API level.
                 // TODO: enable detectImplicitDirectBoot() once system is less noisy
@@ -1140,6 +1153,39 @@
             }
 
             /**
+             * Detects when your app is blocked from launching a background activity or a
+             * PendingIntent created by your app cannot be launched.
+             * <p>
+             * Starting an activity requires <a
+             * href="https://developer.android.com/guide/components/activities/background-starts
+             * ">specific permissions</a> which may depend on the state at runtime and especially
+             * in case of {@link android.app.PendingIntent} starts on the collaborating app.
+             * If the activity start is blocked methods like {@link Context#startActivity(Intent)}
+             * or {@link PendingIntent#send()} have no way to return that information. Instead you
+             * can use this strct mode feature to detect blocked starts.
+             * <p>
+             * Note that in some cases blocked starts may be unavoidable, e.g. when the user clicks
+             * the home button while the app tries to start a new activity.
+             */
+            @SuppressWarnings("BuilderSetStyle")
+            @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE_RO)
+            public @NonNull Builder detectBlockedBackgroundActivityLaunch() {
+                return enable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED);
+            }
+
+            /**
+             * Stops detecting whether your app is blocked from launching a background activity or
+             * a PendingIntent created by your app cannot be launched.
+             * <p>
+             * This disables the effect of {@link #detectBlockedBackgroundActivityLaunch()}.
+             */
+            @SuppressWarnings("BuilderSetStyle")
+            @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE_RO)
+            public @NonNull Builder ignoreBlockedBackgroundActivityLaunch() {
+                return disable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED);
+            }
+
+            /**
              * Crashes the whole process on violation. This penalty runs at the end of all enabled
              * penalties so you'll still get your logging or other violations before the process
              * dies.
@@ -2133,10 +2179,25 @@
                 registerIntentMatchingRestrictionCallback();
             }
 
+            if ((sVmPolicy.mask & DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED) != 0) {
+                registerBackgroundActivityLaunchCallback();
+            }
+
             setBlockGuardVmPolicy(sVmPolicy.mask);
         }
     }
 
+    private static void registerBackgroundActivityLaunchCallback() {
+        try {
+            ActivityTaskManager.getService().registerBackgroundActivityStartCallback(
+                    new BackgroundActivityLaunchCallback());
+        } catch (DeadObjectException e) {
+            // ignore
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException handling StrictMode violation", e);
+        }
+    }
+
     private static final class UnsafeIntentStrictModeCallback
             extends IUnsafeIntentStrictModeCallback.Stub {
         @Override
@@ -2161,6 +2222,16 @@
         }
     }
 
+    private static final class BackgroundActivityLaunchCallback
+            extends IBackgroundActivityLaunchCallback.Stub {
+        @Override
+        public void onBackgroundActivityLaunchAborted(String message) {
+            if (StrictMode.vmBackgroundActivityLaunchEnabled()) {
+                StrictMode.onBackgroundActivityLaunchAborted(message);
+            }
+        }
+    }
+
     /** Gets the current VM policy. */
     public static VmPolicy getVmPolicy() {
         synchronized (StrictMode.class) {
@@ -2236,6 +2307,11 @@
     }
 
     /** @hide */
+    public static boolean vmBackgroundActivityLaunchEnabled() {
+        return (sVmPolicy.mask & DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED) != 0;
+    }
+
+    /** @hide */
     public static void onSqliteObjectLeaked(String message, Throwable originStack) {
         onVmPolicyViolation(new SqliteObjectLeakedViolation(message, originStack));
     }
@@ -2402,6 +2478,11 @@
         onVmPolicyViolation(new UnsafeIntentLaunchViolation(intent, msg + intent));
     }
 
+    /** @hide */
+    public static void onBackgroundActivityLaunchAborted(String message) {
+        onVmPolicyViolation(new BackgroundActivityLaunchViolation(message));
+    }
+
     /** Assume locked until we hear otherwise */
     private static volatile boolean sCeStorageUnlocked = false;
 
diff --git a/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java b/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java
new file mode 100644
index 0000000..aef52c6
--- /dev/null
+++ b/core/java/android/os/strictmode/BackgroundActivityLaunchViolation.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.os.strictmode;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+
+/**
+ * Violation raised when your app is blocked from launching an {@link Activity}
+ * (from the background).
+ * <p>
+ * This occurs when the app:
+ * <ul>
+ *     <li>Does not have sufficient privileges to launch the Activity.</li>
+ *     <li>Has not explicitly opted-in to launch the Activity.</li>
+ * </ul>
+ * Violations may affect the functionality of your app and should be addressed to ensure
+ * proper behavior.
+ * @hide
+ */
+public class BackgroundActivityLaunchViolation extends Violation {
+
+    /** @hide */
+    public BackgroundActivityLaunchViolation(@NonNull String message) {
+        super(message);
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8efbc9c..d19681c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6332,6 +6332,27 @@
         public static final String SCREEN_FLASH_NOTIFICATION_COLOR =
                 "screen_flash_notification_color_global";
 
+
+        /**
+         * A semi-colon separated list of Bluetooth hearing devices' local ambient volume.
+         * Each entry is encoded as a key=value list, separated by commas. Ex:
+         *
+         * "addr=XX:XX:XX:00:11,ambient=20,group_ambient=30;addr=XX:XX:XX:00:22,ambient=50"
+         *
+         * The following keys are supported:
+         * <pre>
+         * addr                 (String)
+         * ambient              (int)
+         * group_ambient        (int)
+         * control_expanded     (boolean)
+         * </pre>
+         *
+         * Each entry must contains "addr" attribute, otherwise it'll be ignored.
+         * @hide
+         */
+        public static final String HEARING_DEVICE_LOCAL_AMBIENT_VOLUME =
+                "hearing_device_local_ambient_volume";
+
         /**
          * IMPORTANT: If you add a new public settings you also have to add it to
          * PUBLIC_SETTINGS below. If the new setting is hidden you have to add
@@ -6476,6 +6497,7 @@
             PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
             PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING);
             PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON);
+            PRIVATE_SETTINGS.add(HEARING_DEVICE_LOCAL_AMBIENT_VOLUME);
         }
 
         /**
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index bd9ab86..a8ab211 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -17,6 +17,7 @@
 package android.service.notification;
 
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -882,6 +883,35 @@
         }
     }
 
+    /**
+     * Creates a conversation notification channel for a given package for a given user.
+     *
+     * <p>This method will throw a security exception if you don't have access to notifications
+     * for the given user.</p>
+     * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+     * device} or be the notification assistant in order to use this method.
+     *
+     * @param pkg The package the channel belongs to.
+     * @param user The user the channel belongs to.
+     * @param parentChannelId The parent channel id of the conversation channel belongs to.
+     * @param conversationId The conversation id of the conversation channel.
+     *
+     * @return The created conversation channel.
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public final @Nullable NotificationChannel createConversationNotificationChannelForPackage(
+        @NonNull String pkg, @NonNull UserHandle user, @NonNull String parentChannelId,
+        @NonNull String conversationId) {
+        if (!isBound()) return null;
+        try {
+            return getNotificationInterface()
+                    .createConversationNotificationChannelForPackageFromPrivilegedListener(
+                            mWrapper, pkg, user, parentChannelId, conversationId);
+        } catch (RemoteException e) {
+            Log.v(TAG, "Unable to contact notification manager", e);
+            throw e.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * Updates a notification channel for a given package for a given user. This should only be used
@@ -890,7 +920,7 @@
      * <p>This method will throw a security exception if you don't have access to notifications
      * for the given user.</p>
      * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
-     * device} in order to use this method.
+     * device} or be the notification assistant in order to use this method.
      *
      * @param pkg The package the channel belongs to.
      * @param user The user the channel belongs to.
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index 51961a8..34e311f 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -57,4 +57,12 @@
   namespace: "systemui"
   description: "Guards the new FLAG_SILENT Notification flag"
   bug: "336488844"
+}
+
+flag {
+   name: "notification_conversation_channel_management"
+   is_exported: true
+   namespace: "systemui"
+   description: "Allows the NAS to create and modify conversation notifications"
+   bug: "373599715"
 }
\ No newline at end of file
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 2ab16e9..7f54cf0 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -221,14 +221,14 @@
 
     /**
      * Wear products currently force a slight scaling transition to wallpapers
-     * when the QSS is opened. However, on Wear 6 (SDK 35) and above, 1P watch faces
+     * when the QSS is opened. However, on Wear 7 (SDK 37) and above, 1P watch faces
      * will be expected to either implement their own scaling, or to override this
      * method to allow the WallpaperController to continue to scale for them.
      *
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
     public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L;
 
     static final class WallpaperCommand {
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 09b2201..e830d89 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -195,3 +195,10 @@
   description: "Feature flag for adding a TYPE_DURATION to TtsSpan"
   bug: "337103893"
 }
+
+flag {
+  name: "deprecate_elegant_text_height_api"
+  namespace: "text"
+  description: "Deprecate the Paint#elegantTextHeight API and stick it to true"
+  bug: "349519475"
+}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 0d55544..00d1570 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -87,6 +87,7 @@
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -463,7 +464,7 @@
         /**
          * Called when new jank classifications are available.
          */
-        void onJankDataAvailable(JankData[] jankData);
+        void onJankDataAvailable(@NonNull List<JankData> jankData);
 
     }
 
@@ -2686,7 +2687,9 @@
      * Adds a callback to be informed about SF's jank classification for this surface.
      * @hide
      */
-    public OnJankDataListenerRegistration addJankDataListener(OnJankDataListener listener) {
+    @NonNull
+    public OnJankDataListenerRegistration addOnJankDataListener(
+            @NonNull OnJankDataListener listener) {
         return new OnJankDataListenerRegistration(this, listener);
     }
 
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d5fc262..d9092ee 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -40,7 +40,6 @@
 import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
-import static android.view.flags.Flags.calculateBoundsInParentFromBoundsInScreen;
 import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
 import static android.view.flags.Flags.sensitiveContentAppProtection;
 import static android.view.flags.Flags.toolkitFrameRateAnimationBugfix25q1;
@@ -968,13 +967,6 @@
     private static boolean sAlwaysRemeasureExactly = false;
 
     /**
-     * When true calculates the bounds in parent from bounds in screen relative to its parents.
-     * This addresses the deprecated API (setBoundsInParent) in Compose, which causes empty
-     * getBoundsInParent call for Compose apps.
-     */
-    private static boolean sCalculateBoundsInParentFromBoundsInScreenFlagValue = false;
-
-    /**
      * When true makes it possible to use onMeasure caches also when the force layout flag is
      * enabled. This helps avoiding multiple measures in the same frame with the same dimensions.
      */
@@ -2564,8 +2556,6 @@
 
         sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
         sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision();
-        sCalculateBoundsInParentFromBoundsInScreenFlagValue =
-                calculateBoundsInParentFromBoundsInScreen();
         sUseMeasureCacheDuringForceLayoutFlagValue = enableUseMeasureCacheDuringForceLayout();
     }
 
@@ -9786,7 +9776,7 @@
             structure.setChildCount(1);
             final ViewStructure root = structure.newChild(0);
             if (info != null) {
-                populateVirtualStructure(root, provider, info, null, forAutofill);
+                populateVirtualStructure(root, provider, info, forAutofill);
                 info.recycle();
             } else {
                 Log.w(AUTOFILL_LOG_TAG, "AccessibilityNodeInfo is null.");
@@ -11085,19 +11075,11 @@
 
     private void populateVirtualStructure(ViewStructure structure,
             AccessibilityNodeProvider provider, AccessibilityNodeInfo info,
-            @Nullable AccessibilityNodeInfo parentInfo, boolean forAutofill) {
+            boolean forAutofill) {
         structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
                 null, null, info.getViewIdResourceName());
         Rect rect = structure.getTempRect();
-        // The bounds in parent for Jetpack Compose views aren't set as setBoundsInParent is
-        // deprecated, and only setBoundsInScreen is called.
-        // The bounds in parent can be calculated by diff'ing the child view's bounds in screen with
-        // the parent's.
-        if (sCalculateBoundsInParentFromBoundsInScreenFlagValue) {
-            getBoundsInParent(info, parentInfo, rect);
-        } else {
-            info.getBoundsInParent(rect);
-        }
+        info.getBoundsInParent(rect);
         structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
         structure.setVisibility(VISIBLE);
         structure.setEnabled(info.isEnabled());
@@ -11181,32 +11163,13 @@
                         AccessibilityNodeInfo.getVirtualDescendantId(info.getChildId(i)));
                 if (cinfo != null) {
                     ViewStructure child = structure.newChild(i);
-                    populateVirtualStructure(child, provider, cinfo, info, forAutofill);
+                    populateVirtualStructure(child, provider, cinfo, forAutofill);
                     cinfo.recycle();
                 }
             }
         }
     }
 
-    private void getBoundsInParent(@NonNull AccessibilityNodeInfo info,
-            @Nullable AccessibilityNodeInfo parentInfo, @NonNull Rect rect) {
-        info.getBoundsInParent(rect);
-        // Fallback to calculate bounds in parent by diffing the bounds in
-        // screen if it's all 0.
-        if ((rect.left | rect.top | rect.right | rect.bottom) == 0) {
-            if (parentInfo != null) {
-                Rect parentBoundsInScreen = parentInfo.getBoundsInScreen();
-                Rect boundsInScreen = info.getBoundsInScreen();
-                rect.set(boundsInScreen.left - parentBoundsInScreen.left,
-                        boundsInScreen.top - parentBoundsInScreen.top,
-                        boundsInScreen.right - parentBoundsInScreen.left,
-                        boundsInScreen.bottom - parentBoundsInScreen.top);
-            } else {
-                info.getBoundsInScreen(rect);
-            }
-        }
-    }
-
     /**
      * Dispatch creation of {@link ViewStructure} down the hierarchy.  The default
      * implementation calls {@link #onProvideStructure} and
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 820a1fb..ffa819f 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -40,6 +40,13 @@
 }
 
 flag {
+    name: "a11y_character_in_window_api"
+    namespace: "accessibility"
+    description: "Enables new extra data key for an AccessibilityService to request character bounds in unmagnified window coordinates."
+    bug: "375429616"
+}
+
+flag {
     namespace: "accessibility"
     name: "allow_shortcut_chooser_on_lockscreen"
     description: "Allows the a11y shortcut disambig dialog to appear on the lockscreen"
diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl
index aeb746c..39092fe 100644
--- a/core/java/android/webkit/IWebViewUpdateService.aidl
+++ b/core/java/android/webkit/IWebViewUpdateService.aidl
@@ -72,16 +72,6 @@
     PackageInfo getCurrentWebViewPackage();
 
     /**
-     * Used by Settings to determine whether multiprocess is enabled.
-     */
-    boolean isMultiProcessEnabled();
-
-    /**
-     * Used by Settings to enable/disable multiprocess.
-     */
-    void enableMultiProcess(boolean enable);
-
-    /**
      * Used by Settings to get the default WebView package.
      */
     WebViewProviderInfo getDefaultWebViewPackage();
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 4c5802c..778a51e 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -28,7 +28,6 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.RecordingCanvas;
-import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.SparseArray;
@@ -217,19 +216,7 @@
      * Returns whether WebView should run in multiprocess mode.
      */
     public boolean isMultiProcessEnabled() {
-        if (Flags.updateServiceV2()) {
-            return true;
-        } else if (Flags.updateServiceIpcWrapper()) {
-            // We don't want to support this method in the new wrapper because updateServiceV2 is
-            // intended to ship in the same release (or sooner). It's only possible to disable it
-            // with an obscure adb command, so just return true here too.
-            return true;
-        }
-        try {
-            return WebViewFactory.getUpdateService().isMultiProcessEnabled();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        return true;
     }
 
     /**
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index e10a398..de303f8 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -16,8 +16,6 @@
 
 package android.webkit;
 
-import static android.webkit.Flags.updateServiceV2;
-
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.annotation.UptimeMillisLong;
@@ -490,7 +488,7 @@
                 Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
             }
 
-            if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) {
+            if (!isInstalledPackage(newPackageInfo)) {
                 throw new MissingWebViewPackageException(
                         TextUtils.formatSimple(
                                 "Current WebView Package (%s) is not installed for the current "
@@ -498,7 +496,7 @@
                                 newPackageInfo.packageName));
             }
 
-            if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) {
+            if (!isEnabledPackage(newPackageInfo)) {
                 throw new MissingWebViewPackageException(
                         TextUtils.formatSimple(
                                 "Current WebView Package (%s) is not enabled for the current user",
diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java
index 668cd01..10d16af 100644
--- a/core/java/android/webkit/WebViewZygote.java
+++ b/core/java/android/webkit/WebViewZygote.java
@@ -16,8 +16,6 @@
 
 package android.webkit;
 
-import static android.webkit.Flags.updateServiceV2;
-
 import android.content.pm.PackageInfo;
 import android.os.Build;
 import android.os.ChildZygoteProcess;
@@ -52,13 +50,6 @@
     @GuardedBy("sLock")
     private static PackageInfo sPackage;
 
-    /**
-     * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote will
-     * not be started. Should be removed entirely after we remove the updateServiceV2 flag.
-     */
-    @GuardedBy("sLock")
-    private static boolean sMultiprocessEnabled = false;
-
     public static ZygoteProcess getProcess() {
         synchronized (sLock) {
             if (sZygote != null) return sZygote;
@@ -76,40 +67,13 @@
 
     public static boolean isMultiprocessEnabled() {
         synchronized (sLock) {
-            if (updateServiceV2()) {
-                return sPackage != null;
-            } else {
-                return sMultiprocessEnabled && sPackage != null;
-            }
-        }
-    }
-
-    public static void setMultiprocessEnabled(boolean enabled) {
-        if (updateServiceV2()) {
-            throw new IllegalStateException(
-                    "setMultiprocessEnabled shouldn't be called if update_service_v2 flag is set.");
-        }
-        synchronized (sLock) {
-            sMultiprocessEnabled = enabled;
-
-            // When multi-process is disabled, kill the zygote. When it is enabled,
-            // the zygote will be started when it is first needed in getProcess().
-            if (!enabled) {
-                stopZygoteLocked();
-            }
+            return sPackage != null;
         }
     }
 
     static void onWebViewProviderChanged(PackageInfo packageInfo) {
         synchronized (sLock) {
             sPackage = packageInfo;
-
-            // If multi-process is not enabled, then do not start the zygote service.
-            // Only check sMultiprocessEnabled if updateServiceV2 is not enabled.
-            if (!updateServiceV2() && !sMultiprocessEnabled) {
-                return;
-            }
-
             stopZygoteLocked();
         }
     }
diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java
index 255bd67..65cd190 100644
--- a/core/java/android/widget/TextClock.java
+++ b/core/java/android/widget/TextClock.java
@@ -45,6 +45,7 @@
 import com.android.internal.R;
 import com.android.internal.util.Preconditions;
 
+import java.time.DateTimeException;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.ZoneId;
@@ -291,11 +292,26 @@
     }
 
     private void createTime(String timeZone) {
-        if (timeZone != null) {
-            mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
+        TimeZone tz = null;
+        if (timeZone == null) {
+            tz = TimeZone.getDefault();
+            // Note that mTimeZone should always be null if timeZone is.
         } else {
-            mTime = Calendar.getInstance();
+            tz = TimeZone.getTimeZone(timeZone);
+            try {
+                // Try converting this TZ to a zoneId to make sure it's valid. This
+                // performs a different set of checks than TimeZone.getTimeZone so
+                // we can avoid exceptions later when we do need this conversion.
+                tz.toZoneId();
+            } catch (DateTimeException ex) {
+                // If we're here, the user supplied timezone is invalid, so reset
+                // mTimeZone to something sane.
+                tz = TimeZone.getDefault();
+                mTimeZone = tz.getID();
+            }
         }
+
+        mTime = Calendar.getInstance(tz);
     }
 
     /**
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index 6cefc4d..4b7bacb 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -87,6 +87,13 @@
      */
     public static final String KEY_GESTURE_FINISHED = "GestureFinished";
 
+    /**
+     * Touch gestured has transferred to embedded window, Shell should pilfer pointers so the
+     * embedded won't receive motion events.
+     * @hide
+     */
+    public static final String KEY_TOUCH_GESTURE_TRANSFERRED = "TouchGestureTransferred";
+
 
     /**
      * Defines the type of back destinations a back even can lead to. This is used to define the
@@ -119,7 +126,7 @@
     @NonNull
     private final Rect mTouchableRegion;
 
-    private final boolean mAppProgressGenerationAllowed;
+    private boolean mAppProgressGenerationAllowed;
     private final int mFocusedTaskId;
 
     /**
@@ -253,6 +260,14 @@
     }
 
     /**
+     * Force disable app to intercept back progress event.
+     * @hide
+     */
+    public void disableAppProgressGenerationAllowed() {
+        mAppProgressGenerationAllowed = false;
+    }
+
+    /**
      * Callback to be called when the back preview is finished in order to notify the server that
      * it can clean up the resources created for the animation.
      * @hide
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 22eec29..dae87dd 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -70,7 +70,12 @@
     ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
     ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false),
     ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
-            Flags::enableWindowingTransitionHandlersObservers, false);
+            Flags::enableWindowingTransitionHandlersObservers, false),
+    ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS(
+            Flags::enableDesktopAppLaunchAlttabTransitions, false),
+    ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS(
+            Flags::enableDesktopAppLaunchTransitions, false),
+    ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false);
 
     private static final String TAG = "DesktopModeFlagsUtil";
     // Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 57aacff..a2201bb 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -71,6 +71,7 @@
     bug: "339720406"
 }
 
+# replaced by bal_strict_mode_ro
 flag {
     name: "bal_strict_mode"
     namespace: "responsible_apis"
@@ -79,6 +80,14 @@
 }
 
 flag {
+    name: "bal_strict_mode_ro"
+    namespace: "responsible_apis"
+    description: "Strict mode flag"
+    bug: "324089586"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "bal_reduce_grace_period"
     namespace: "responsible_apis"
     description: "Changes to reduce or ideally remove the grace period exemption."
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index ed7a796..3a03508 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -348,3 +348,14 @@
   bug: "372230928"
   is_fixed_read_only: true
 }
+
+flag {
+  name: "disallow_app_progress_embedded_window"
+  namespace: "windowing_frontend"
+  description: "Pilfer pointers when app transfer input gesture to embedded window."
+  bug: "365504126"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index be3f10a..091975c 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -28,6 +28,7 @@
 import android.widget.Filter;
 import android.widget.Filterable;
 import android.widget.LinearLayout;
+import android.widget.RadioButton;
 import android.widget.TextView;
 
 import com.android.internal.R;
@@ -242,11 +243,7 @@
                 break;
             case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER:
                 TextView title;
-                LocaleStore.LocaleInfo info = (LocaleStore.LocaleInfo) getItem(position);
-                if (info == null) {
-                    throw new NullPointerException("Non header locale cannot be null.");
-                }
-                if (info.isAppCurrentLocale()) {
+                if (mHasSpecificAppPackageName) {
                     title = itemView.findViewById(R.id.language_picker_item);
                 } else {
                     title = itemView.findViewById(R.id.locale);
@@ -254,11 +251,22 @@
                 title.setText(R.string.system_locale_title);
                 break;
             case TYPE_CURRENT_LOCALE:
-                updateTextView(itemView,
-                        itemView.findViewById(R.id.language_picker_item), position);
+                updateTextView(
+                        itemView, itemView.findViewById(R.id.language_picker_item), position);
                 break;
             default:
-                updateTextView(itemView, itemView.findViewById(R.id.locale), position);
+                LocaleStore.LocaleInfo localeInfo = (LocaleStore.LocaleInfo) getItem(position);
+                if (localeInfo == null) {
+                    throw new NullPointerException("Non header locale cannot be null.");
+                }
+                if (mHasSpecificAppPackageName && localeInfo.isSuggested() && !mCountryMode) {
+                    updateTextView(
+                            itemView,
+                            itemView.findViewById(R.id.language_picker_item),
+                            position);
+                } else {
+                    updateTextView(itemView, itemView.findViewById(R.id.locale), position);
+                }
                 break;
         }
         return itemView;
@@ -280,20 +288,21 @@
                 }
                 break;
             case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER:
-                if (((LocaleStore.LocaleInfo) getItem(position)).isAppCurrentLocale()) {
-                    shouldReuseView = convertView instanceof LinearLayout
-                            && convertView.findViewById(R.id.language_picker_item) != null;
-                    if (!shouldReuseView) {
-                        updatedView = mInflater.inflate(
-                                R.layout.app_language_picker_current_locale_item,
-                                parent, false);
+                if (mHasSpecificAppPackageName) {
+                    updatedView = mInflater.inflate(
+                        R.layout.app_language_picker_locale_item, parent, false);
+                    RadioButton option = updatedView.findViewById(R.id.checkbox);
+                    if (((LocaleStore.LocaleInfo) getItem(position)).isAppCurrentLocale()) {
+                        option.setChecked(true);
+                    } else {
+                        option.setChecked(false);
                     }
                 } else {
                     shouldReuseView = convertView instanceof TextView
-                            && convertView.findViewById(R.id.locale) != null;
+                        && convertView.findViewById(R.id.locale) != null;
                     if (!shouldReuseView) {
-                        updatedView = mInflater.inflate(
-                                R.layout.language_picker_item, parent, false);
+                        updatedView =
+                            mInflater.inflate(R.layout.language_picker_item, parent, false);
                     }
                 }
                 break;
@@ -302,14 +311,30 @@
                         && convertView.findViewById(R.id.language_picker_item) != null;
                 if (!shouldReuseView) {
                     updatedView = mInflater.inflate(
-                            R.layout.app_language_picker_current_locale_item, parent, false);
+                            R.layout.app_language_picker_locale_item, parent, false);
+                    RadioButton option = updatedView.findViewById(R.id.checkbox);
+                    option.setChecked(true);
                 }
                 break;
             default:
-                shouldReuseView = convertView instanceof TextView
+                LocaleStore.LocaleInfo localeInfo = (LocaleStore.LocaleInfo) getItem(position);
+                if (mHasSpecificAppPackageName
+                        && localeInfo.isSuggested() && !mCountryMode) {
+                    shouldReuseView = convertView instanceof LinearLayout
+                        && convertView.findViewById(R.id.language_picker_item) != null;
+                    if ((!shouldReuseView)) {
+                        updatedView = mInflater.inflate(
+                            R.layout.app_language_picker_locale_item, parent, false);
+                        RadioButton option = updatedView.findViewById(R.id.checkbox);
+                        option.setChecked(false);
+                    }
+                } else {
+                    shouldReuseView = convertView instanceof TextView
                         && convertView.findViewById(R.id.locale) != null;
-                if (!shouldReuseView) {
-                    updatedView = mInflater.inflate(R.layout.language_picker_item, parent, false);
+                    if ((!shouldReuseView)) {
+                        updatedView = mInflater.inflate(
+                            R.layout.language_picker_item, parent, false);
+                    }
                 }
                 break;
         }
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 0af4bea..44c0bd0 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -54,6 +54,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -448,7 +449,7 @@
     }
 
     @Override
-    public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
+    public void onJankDataAvailable(List<SurfaceControl.JankData> jankData) {
         postCallback(() -> {
             try {
                 Trace.beginSection("FrameTracker#onJankDataAvailable");
@@ -832,7 +833,7 @@
         /** adds the jank listener to the given surface */
         public SurfaceControl.OnJankDataListenerRegistration addJankStatsListener(
                 SurfaceControl.OnJankDataListener listener, SurfaceControl surfaceControl) {
-            return surfaceControl.addJankDataListener(listener);
+            return surfaceControl.addOnJankDataListener(listener);
         }
     }
 
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 032ac42..c182b4a 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -421,6 +421,8 @@
     @NonNull
     private String[] mUsesStaticLibrariesSorted;
 
+    private Map<String, Boolean> mFeatureFlagState = new ArrayMap<>();
+
     @NonNull
     public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath,
             @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp,
@@ -2819,6 +2821,7 @@
         queriesProviders = Collections.unmodifiableSet(queriesProviders);
         mimeGroups = Collections.unmodifiableSet(mimeGroups);
         mKnownActivityEmbeddingCerts = Collections.unmodifiableSet(mKnownActivityEmbeddingCerts);
+        mFeatureFlagState = Collections.unmodifiableMap(mFeatureFlagState);
     }
 
     @Override
@@ -3118,6 +3121,8 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        writeFeatureFlagState(dest);
+
         sForBoolean.parcel(this.supportsSmallScreens, dest, flags);
         sForBoolean.parcel(this.supportsNormalScreens, dest, flags);
         sForBoolean.parcel(this.supportsLargeScreens, dest, flags);
@@ -3267,6 +3272,27 @@
         dest.writeBoolean(this.mAllowCrossUidActivitySwitchFromBelow);
     }
 
+    private void writeFeatureFlagState(@NonNull Parcel dest) {
+        // Use a string array to encode flag state. One string per flag in the form `<flag>=<value>`
+        // where value is 0 (disabled), 1 (enabled) or ? (unknown flag or value).
+        int featureFlagCount = this.mFeatureFlagState.size();
+        String[] featureFlagStateAsArray = new String[featureFlagCount];
+        var entryIterator = this.mFeatureFlagState.entrySet().iterator();
+        for (int i = 0; i < featureFlagCount; i++) {
+            var entry = entryIterator.next();
+            Boolean flagValue = entry.getValue();
+            if (flagValue == null) {
+                featureFlagStateAsArray[i] = entry.getKey() + "=?";
+            } else if (flagValue.booleanValue()) {
+                featureFlagStateAsArray[i] = entry.getKey() + "=1";
+            } else {
+                featureFlagStateAsArray[i] = entry.getKey() + "=0";
+            }
+
+        }
+        dest.writeStringArray(featureFlagStateAsArray);
+    }
+
     public PackageImpl(Parcel in) {
         this(in, /* callback */ null);
     }
@@ -3275,6 +3301,9 @@
         mCallback = callback;
         // We use the boot classloader for all classes that we load.
         final ClassLoader boot = Object.class.getClassLoader();
+
+        readFeatureFlagState(in);
+
         this.supportsSmallScreens = sForBoolean.unparcel(in);
         this.supportsNormalScreens = sForBoolean.unparcel(in);
         this.supportsLargeScreens = sForBoolean.unparcel(in);
@@ -3440,6 +3469,27 @@
         // to mutate this instance before it's finalized.
     }
 
+    private void readFeatureFlagState(@NonNull Parcel in) {
+        // See comment in writeFeatureFlagState() for encoding of flag state.
+        String[] featureFlagStateAsArray = in.createStringArray();
+        for (String s : featureFlagStateAsArray) {
+            int sepIndex = s.lastIndexOf('=');
+            if (sepIndex >= 0 && sepIndex == s.length() - 2) {
+                String flagPackageAndName = s.substring(0, sepIndex);
+                char c = s.charAt(sepIndex + 1);
+                Boolean flagValue = null;
+                if (c == '1') {
+                    flagValue = Boolean.TRUE;
+                } else if (c == '0') {
+                    flagValue = Boolean.FALSE;
+                } else if (c != '?') {
+                    continue;
+                }
+                this.mFeatureFlagState.put(flagPackageAndName, flagValue);
+            }
+        }
+    }
+
     @NonNull
     public static final Creator<PackageImpl> CREATOR = new Creator<PackageImpl>() {
         @Override
@@ -3660,6 +3710,18 @@
         return mBaseAppDataDeviceProtectedDirForSystemUser;
     }
 
+    @Override
+    public PackageImpl addFeatureFlag(
+            @NonNull String flagPackageAndName,
+            @Nullable Boolean flagValue) {
+        mFeatureFlagState.put(flagPackageAndName, flagValue);
+        return this;
+    }
+
+    public Map<String, Boolean> getFeatureFlagState() {
+        return mFeatureFlagState;
+    }
+
     /**
      * Flags used for a internal bitset. These flags should never be persisted or exposed outside
      * of this class. It is expected that PackageCacher explicitly clears itself whenever the
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 8faaf95..70b7953 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -33,6 +33,7 @@
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.pkg.parsing.ParsingPackage;
 import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -199,7 +200,7 @@
      * @return the current value of the given Aconfig flag, or null if there is no such flag
      */
     @Nullable
-    private Boolean getFlagValue(@NonNull String flagPackageAndName) {
+    public Boolean getFlagValue(@NonNull String flagPackageAndName) {
         Boolean value = mFlagValues.get(flagPackageAndName);
         if (DEBUG) {
             Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
@@ -209,10 +210,13 @@
 
     /**
      * Check if the element in {@code parser} should be skipped because of the feature flag.
+     * @param pkg The package being parsed
      * @param parser XML parser object currently parsing an element
      * @return true if the element is disabled because of its feature flag
      */
-    public boolean skipCurrentElement(@NonNull XmlResourceParser parser) {
+    public boolean skipCurrentElement(
+            @NonNull ParsingPackage pkg,
+            @NonNull XmlResourceParser parser) {
         if (!Flags.manifestFlagging()) {
             return false;
         }
@@ -227,18 +231,21 @@
             featureFlag = featureFlag.substring(1).strip();
         }
         final Boolean flagValue = getFlagValue(featureFlag);
+        boolean shouldSkip = false;
         if (flagValue == null) {
             Slog.w(LOG_TAG, "Skipping element " + parser.getName()
                     + " due to unknown feature flag " + featureFlag);
-            return true;
-        }
-        // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated)
-        if (flagValue == negated) {
+            shouldSkip = true;
+        } else if (flagValue == negated) {
+            // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated)
             Slog.i(LOG_TAG, "Skipping element " + parser.getName()
                     + " behind feature flag " + featureFlag + " = " + flagValue);
-            return true;
+            shouldSkip = true;
         }
-        return false;
+        if (android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) {
+            pkg.addFeatureFlag(featureFlag, flagValue);
+        }
+        return shouldSkip;
     }
 
     /**
diff --git a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java
index 8858f94..335dedd 100644
--- a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java
@@ -61,7 +61,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java
index bb01581..5f48d16 100644
--- a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java
+++ b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java
@@ -54,7 +54,7 @@
             return input.skip("install-constraints cannot be used by this package");
         }
 
-        ParseResult<Set<String>> prefixes = parseFingerprintPrefixes(input, res, parser);
+        ParseResult<Set<String>> prefixes = parseFingerprintPrefixes(input, pkg, res, parser);
         if (prefixes.isSuccess()) {
             if (validateFingerprintPrefixes(prefixes.getResult())) {
                 return input.success(pkg);
@@ -68,7 +68,7 @@
     }
 
     private static ParseResult<Set<String>> parseFingerprintPrefixes(
-            ParseInput input, Resources res, XmlResourceParser parser)
+            ParseInput input, ParsingPackage pkg, Resources res, XmlResourceParser parser)
             throws XmlPullParserException, IOException {
         Set<String> prefixes = new ArraySet<>();
         int type;
@@ -81,7 +81,7 @@
                 }
                 return input.success(prefixes);
             } else if (type == XmlPullParser.START_TAG) {
-                if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+                if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                     continue;
                 }
                 if (parser.getName().equals(TAG_FINGERPRINT_PREFIX)) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
index 55baa53..a35150b 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
@@ -393,7 +393,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
index da48b23..39d7af6 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java
@@ -99,7 +99,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -200,7 +200,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 6af2a29..3726b42 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -174,7 +174,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index c68ea2d..9601813 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -138,7 +138,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) {
+            if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
index 5d185af..341beed 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java
@@ -123,6 +123,9 @@
 
     ParsingPackage addQueriesProvider(String authority);
 
+    /** Adds a feature flag (`android:featureFlag` attribute) encountered in the manifest. */
+    ParsingPackage addFeatureFlag(@NonNull String flagPackageAndName, @Nullable Boolean flagValue);
+
     /** Sets a process name -> {@link ParsedProcess} map coming from the <processes> tag. */
     ParsingPackage setProcesses(@NonNull Map<String, ParsedProcess> processes);
 
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
index 787006e..bb733f2 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@@ -763,7 +763,7 @@
             if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -842,7 +842,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -988,7 +988,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -1610,7 +1610,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -1853,7 +1853,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
             if (parser.getName().equals("intent")) {
@@ -2202,7 +2202,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
@@ -2620,7 +2620,7 @@
                 }
             }
 
-            ParseResult<String[]> certResult = parseAdditionalCertificates(input, res, parser);
+            ParseResult<String[]> certResult = parseAdditionalCertificates(input, pkg, res, parser);
             if (certResult.isError()) {
                 return input.error(certResult);
             }
@@ -2674,7 +2674,8 @@
             // Fot apps targeting O-MR1 we require explicit enumeration of all certs.
             String[] additionalCertSha256Digests = EmptyArray.STRING;
             if (pkg.getTargetSdkVersion() >= Build.VERSION_CODES.O_MR1) {
-                ParseResult<String[]> certResult = parseAdditionalCertificates(input, res, parser);
+                ParseResult<String[]> certResult =
+                        parseAdditionalCertificates(input, pkg, res, parser);
                 if (certResult.isError()) {
                     return input.error(certResult);
                 }
@@ -2782,7 +2783,7 @@
     }
 
     private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input,
-            Resources resources, XmlResourceParser parser)
+            ParsingPackage pkg, Resources resources, XmlResourceParser parser)
             throws XmlPullParserException, IOException {
         String[] certSha256Digests = EmptyArray.STRING;
         final int depth = parser.getDepth();
@@ -2793,7 +2794,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            if (sAconfigFlags.skipCurrentElement(parser)) {
+            if (sAconfigFlags.skipCurrentElement(pkg, parser)) {
                 continue;
             }
 
diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
index febe1f3..70d148a 100644
--- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
@@ -132,9 +132,9 @@
     @Deprecated
     @Override
     void dumpViewerConfig() {
-        Log.d(LOG_TAG, "Dumping viewer config to trace");
+        Log.d(LOG_TAG, "Dumping viewer config to trace from " + mViewerConfigFilePath);
         Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider);
-        Log.d(LOG_TAG, "Dumped viewer config to trace");
+        Log.d(LOG_TAG, "Successfully dumped viewer config to trace from " + mViewerConfigFilePath);
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/FgThread.java b/core/java/com/android/server/FgThread.java
similarity index 97%
rename from services/core/java/com/android/server/FgThread.java
rename to core/java/com/android/server/FgThread.java
index b4b6e3f..f8a6bb0 100644
--- a/services/core/java/com/android/server/FgThread.java
+++ b/core/java/com/android/server/FgThread.java
@@ -30,7 +30,10 @@
  * relatively long-running operations like saving state to disk (in addition to
  * simply being a background priority), which can cause operations scheduled on it
  * to be delayed for a user-noticeable amount of time.
+ *
+ * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class FgThread extends ServiceThread {
     private static final long SLOW_DISPATCH_THRESHOLD_MS = 100;
     private static final long SLOW_DELIVERY_THRESHOLD_MS = 200;
diff --git a/services/core/java/com/android/server/ServiceThread.java b/core/java/com/android/server/ServiceThread.java
similarity index 95%
rename from services/core/java/com/android/server/ServiceThread.java
rename to core/java/com/android/server/ServiceThread.java
index 6d8e49c..86e507b 100644
--- a/services/core/java/com/android/server/ServiceThread.java
+++ b/core/java/com/android/server/ServiceThread.java
@@ -24,7 +24,10 @@
 
 /**
  * Special handler thread that we create for system services that require their own loopers.
+ *
+ * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ServiceThread extends HandlerThread {
     private static final String TAG = "ServiceThread";
 
diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
similarity index 99%
rename from services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
rename to core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
index 1526230..e8aeb86 100644
--- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
+++ b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
@@ -63,6 +63,8 @@
  * <p>Optionally, two permissions may be specified: (1) a caller permission - any service that does
  * not require callers to hold this permission is rejected (2) a service permission - any service
  * whose package does not hold this permission is rejected.
+ *
+ * @hide
  */
 public final class CurrentUserServiceSupplier extends BroadcastReceiver implements
         ServiceSupplier<CurrentUserServiceSupplier.BoundServiceInfo> {
diff --git a/services/core/java/com/android/server/servicewatcher/OWNERS b/core/java/com/android/server/servicewatcher/OWNERS
similarity index 100%
rename from services/core/java/com/android/server/servicewatcher/OWNERS
rename to core/java/com/android/server/servicewatcher/OWNERS
diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java b/core/java/com/android/server/servicewatcher/ServiceWatcher.java
similarity index 92%
rename from services/core/java/com/android/server/servicewatcher/ServiceWatcher.java
rename to core/java/com/android/server/servicewatcher/ServiceWatcher.java
index 5636718..831ff67 100644
--- a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java
+++ b/core/java/com/android/server/servicewatcher/ServiceWatcher.java
@@ -16,6 +16,10 @@
 
 package com.android.server.servicewatcher;
 
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.Context.BIND_NOT_FOREGROUND;
+import static android.content.Context.BIND_NOT_VISIBLE;
+
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.content.ComponentName;
@@ -52,6 +56,8 @@
  * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely
  * on this, and instead use {@link ServiceListener} notifications as necessary to recover from
  * failures.
+ *
+ * @hide
  */
 public interface ServiceWatcher {
 
@@ -144,6 +150,10 @@
         protected final @Nullable String mAction;
         protected final int mUid;
         protected final ComponentName mComponentName;
+        private final int mFlags;
+
+        private static final int DEFAULT_FLAGS =
+                BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE;
 
         protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
             this(action, resolveInfo.serviceInfo.applicationInfo.uid,
@@ -151,9 +161,14 @@
         }
 
         protected BoundServiceInfo(String action, int uid, ComponentName componentName) {
+            this(action, uid, componentName, DEFAULT_FLAGS);
+        }
+
+        protected BoundServiceInfo(String action, int uid, ComponentName componentName, int flags) {
             mAction = action;
             mUid = uid;
             mComponentName = Objects.requireNonNull(componentName);
+            mFlags = flags;
         }
 
         /** Returns the action associated with this bound service. */
@@ -171,6 +186,11 @@
             return UserHandle.getUserId(mUid);
         }
 
+        /** Returns flags used when binding the service. */
+        public int getFlags() {
+            return mFlags;
+        }
+
         @Override
         public final boolean equals(Object o) {
             if (this == o) {
@@ -183,12 +203,13 @@
             BoundServiceInfo that = (BoundServiceInfo) o;
             return mUid == that.mUid
                     && Objects.equals(mAction, that.mAction)
-                    && mComponentName.equals(that.mComponentName);
+                    && mComponentName.equals(that.mComponentName)
+                    && mFlags == that.mFlags;
         }
 
         @Override
         public final int hashCode() {
-            return Objects.hash(mAction, mUid, mComponentName);
+            return Objects.hash(mAction, mUid, mComponentName, mFlags);
         }
 
         @Override
@@ -256,4 +277,4 @@
      * Dumps ServiceWatcher information.
      */
     void dump(PrintWriter pw);
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
similarity index 97%
rename from services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
rename to core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
index b178269..ccbab9f 100644
--- a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
+++ b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
@@ -16,10 +16,6 @@
 
 package com.android.server.servicewatcher;
 
-import static android.content.Context.BIND_AUTO_CREATE;
-import static android.content.Context.BIND_NOT_FOREGROUND;
-import static android.content.Context.BIND_NOT_VISIBLE;
-
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
@@ -46,6 +42,8 @@
  * Implementation of ServiceWatcher. Keeping the implementation separate from the interface allows
  * us to store the generic relationship between the service supplier and the service listener, while
  * hiding the generics from clients, simplifying the API.
+ *
+ * @hide
  */
 class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceWatcher,
         ServiceChangedListener {
@@ -212,7 +210,7 @@
                     mBoundServiceInfo.getComponentName());
             try {
                 if (!mContext.bindServiceAsUser(bindIntent, this,
-                        BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
+                        mBoundServiceInfo.getFlags(),
                         mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) {
                     Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
                     mRebinder = this::bind;
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 2283b88..2bb6e71 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -105,18 +105,7 @@
     ],
 
     shared_libs: [
-        "libbase",
-        "libcutils",
         "libtracing_perfetto",
-        "libharfbuzz_ng",
-        "liblog",
-        "libmediautils",
-        "libminikin",
-        "libz",
-        "server_configurable_flags",
-        "libaconfig_storage_read_api_cc",
-        "android.database.sqlite-aconfig-cc",
-        "android.media.audiopolicy-aconfig-cc",
     ],
 
     static_libs: [
@@ -303,6 +292,14 @@
             ],
 
             shared_libs: [
+                "libbase",
+                "libharfbuzz_ng",
+                "liblog",
+                "libmediautils",
+                "libminikin",
+                "libz",
+                "android.database.sqlite-aconfig-cc",
+                "android.media.audiopolicy-aconfig-cc",
                 "audioclient-types-aidl-cpp",
                 "audioflinger-aidl-cpp",
                 "audiopolicy-types-aidl-cpp",
@@ -412,20 +409,24 @@
                 "frameworks/native/libs/nativebase/include",
                 "frameworks/native/libs/nativewindow/include",
             ],
-            shared_libs: [
-                "libicui18n",
-                "libicuuc",
-            ],
             static_libs: [
                 "libandroidfw",
+                "libbase",
                 "libbinary_parse",
+                "libcutils",
                 "libdng_sdk",
                 "libft2",
+                "libharfbuzz_ng",
                 "libhostgraphics",
                 "libhwui",
+                "libicui18n",
+                "libicuuc",
+                "libicuuc_stubdata",
                 "libimage_type_recognition",
                 "libinput",
                 "libjpeg",
+                "liblog",
+                "libminikin",
                 "libnativehelper_jvm",
                 "libpiex",
                 "libpng",
@@ -435,11 +436,18 @@
                 "libwebp-decode",
                 "libwebp-encode",
                 "libwuffs_mirror_release_c",
+                "libz",
                 "libimage_io",
                 "libjpegdecoder",
                 "libjpegencoder",
                 "libultrahdr",
+                "server_configurable_flags",
             ],
+            export_static_lib_headers: [
+                "libnativehelper_jvm",
+                "libui-types",
+            ],
+            stl: "libc++_static",
         },
         host_linux: {
             srcs: [
@@ -465,14 +473,18 @@
                 "libbinderthreadstateutils",
                 "libsqlite",
                 "libgui_window_info_static",
-            ],
-            shared_libs: [
-                // libbinder needs to be shared since it has global state
-                // (e.g. gDefaultServiceManager)
                 "libbinder",
                 "libhidlbase", // libhwbinder is in here
             ],
         },
+        linux_glibc_x86_64: {
+            ldflags: ["-static-libgcc"],
+            dist: {
+                targets: ["layoutlib"],
+                dir: "layoutlib_native/linux",
+                tag: "stripped_all",
+            },
+        },
     },
 }
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 7fefe17..df87a69 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -298,6 +298,11 @@
     jmethodID ctor;
 } gFrameRateCategoryRateClassInfo;
 
+static struct {
+    jclass clazz;
+    jmethodID asList;
+} gUtilArrays;
+
 constexpr ui::Dataspace pickDataspaceFromColorMode(const ui::ColorMode colorMode) {
     switch (colorMode) {
         case ui::ColorMode::DISPLAY_P3:
@@ -2206,10 +2211,13 @@
             env->SetObjectArrayElement(jJankDataArray, i, jJankData);
             env->DeleteLocalRef(jJankData);
         }
-        env->CallVoidMethod(target,
-                gJankDataListenerClassInfo.onJankDataAvailable,
-                jJankDataArray);
+
+        jobject jJankDataList =
+                env->CallStaticObjectMethod(gUtilArrays.clazz, gUtilArrays.asList, jJankDataArray);
         env->DeleteLocalRef(jJankDataArray);
+
+        env->CallVoidMethod(target, gJankDataListenerClassInfo.onJankDataAvailable, jJankDataList);
+        env->DeleteLocalRef(jJankDataList);
         env->DeleteLocalRef(target);
 
         return true;
@@ -2858,7 +2866,7 @@
     gJankDataListenerClassInfo.clazz = MakeGlobalRefOrDie(env, onJankDataListenerClazz);
     gJankDataListenerClassInfo.onJankDataAvailable =
             GetMethodIDOrDie(env, onJankDataListenerClazz, "onJankDataAvailable",
-                             "([Landroid/view/SurfaceControl$JankData;)V");
+                             "(Ljava/util/List;)V");
 
     jclass transactionCommittedListenerClazz =
             FindClassOrDie(env, "android/view/SurfaceControl$TransactionCommittedListener");
@@ -2933,6 +2941,10 @@
     gStalledTransactionInfoClassInfo.frameNumber =
             GetFieldIDOrDie(env, stalledTransactionInfoClazz, "frameNumber", "J");
 
+    jclass utilArrays = FindClassOrDie(env, "java/util/Arrays");
+    gUtilArrays.clazz = MakeGlobalRefOrDie(env, utilArrays);
+    gUtilArrays.asList = GetStaticMethodIDOrDie(env, utilArrays, "asList",
+                                                "([Ljava/lang/Object;)Ljava/util/List;");
     return err;
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 561d351..5044a30 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7023,7 +7023,6 @@
     <!-- Allows an application to set the advanced features on BiometricDialog (SystemUI), including
          logo, logo description, and content view with more options button.
          <p>Not for use by third-party applications.
-         @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt")
     -->
     <permission android:name="android.permission.SET_BIOMETRIC_DIALOG_ADVANCED"
                 android:protectionLevel="signature|privileged" />
@@ -8536,10 +8535,14 @@
         @hide
     -->
     <permission
-        android:name="android.permission.RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
+        android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
         android:protectionLevel="internal"
         android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/>
 
+    <uses-permission
+        android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
+        android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/>
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_locale_item.xml
similarity index 73%
rename from core/res/res/layout/app_language_picker_current_locale_item.xml
rename to core/res/res/layout/app_language_picker_locale_item.xml
index edd6d64..bcad9ce 100644
--- a/core/res/res/layout/app_language_picker_current_locale_item.xml
+++ b/core/res/res/layout/app_language_picker_locale_item.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ 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.
@@ -20,10 +20,27 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:gravity="center_vertical">
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:minHeight="?android:attr/listPreferredItemHeight"
+        android:layout_marginStart="20dip">
+
+      <RadioButton
+          android:id="@+id/checkbox"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:layout_gravity="center"
+          android:background="@null"
+          android:focusable="false"
+          android:clickable="false" />
+
+    </LinearLayout>
+
     <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginEnd="6dip"
         android:layout_marginTop="6dip"
         android:layout_marginBottom="6dip"
         android:layout_weight="1">
@@ -31,20 +48,4 @@
             android:id="@+id/language_picker_item"
             layout="@layout/language_picker_item" />
     </RelativeLayout>
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center"
-        android:minHeight="?android:attr/listPreferredItemHeight">
-        <ImageView
-            android:id="@+id/imageView"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_centerVertical="true"
-            android:layout_marginHorizontal="16dp"
-            android:src="@drawable/ic_check_24dp"
-            app:tint="?attr/colorAccentPrimaryVariant"
-            android:contentDescription="@*android:string/checked"/>
-    </LinearLayout>
 </LinearLayout>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 0d4870b..d3ef07ca 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5337,7 +5337,9 @@
     <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] -->
     <string name="zen_mode_default_every_night_name">Sleeping</string>
 
-    <!-- Zen mode - Trigger description of the rule, indicating which app owns it. [CHAR_LIMIT=100] -->
+    <!-- Implicit zen mode - Name of the rule, indicating which app owns it. [CHAR_LIMIT=30] -->
+    <string name="zen_mode_implicit_name">Do Not Disturb (<xliff:g id="app_name" example="Gmail">%1$s</xliff:g>)</string>
+    <!-- Implicit zen mode - Trigger description of the rule, indicating which app owns it. [CHAR_LIMIT=100] -->
     <string name="zen_mode_implicit_trigger_description">Managed by <xliff:g id="app_name">%1$s</xliff:g></string>
     <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
     <string name="zen_mode_implicit_activated">On</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 51c405ae..515ebd5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2652,6 +2652,7 @@
   <java-symbol type="string" name="zen_mode_default_weekends_name" />
   <java-symbol type="string" name="zen_mode_default_events_name" />
   <java-symbol type="string" name="zen_mode_default_every_night_name" />
+  <java-symbol type="string" name="zen_mode_implicit_name" />
   <java-symbol type="string" name="zen_mode_implicit_trigger_description" />
   <java-symbol type="string" name="zen_mode_implicit_activated" />
   <java-symbol type="string" name="zen_mode_implicit_deactivated" />
@@ -5263,7 +5264,7 @@
   <java-symbol type="string" name="system_locale_title" />
   <java-symbol type="layout" name="app_language_picker_system_default" />
   <java-symbol type="layout" name="app_language_picker_system_current" />
-  <java-symbol type="layout" name="app_language_picker_current_locale_item" />
+  <java-symbol type="layout" name="app_language_picker_locale_item" />
   <java-symbol type="id" name="system_locale_subtitle" />
   <java-symbol type="id" name="language_picker_item" />
   <java-symbol type="id" name="language_picker_header" />
diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
index 60b5a42..e7a6cb7 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -67,6 +67,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
+import java.util.Arrays;
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
@@ -690,10 +691,9 @@
             FrameTracker tracker, long durationMillis, long vsyncId, @JankType int jankType) {
         final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
         doNothing().when(tracker).postCallback(captor.capture());
-        mListenerCapture.getValue().onJankDataAvailable(new JankData[] {
-                new JankData(vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz,
-                TimeUnit.MILLISECONDS.toNanos(durationMillis))
-        });
+        mListenerCapture.getValue().onJankDataAvailable(Arrays.asList(new JankData(
+                vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz,
+                TimeUnit.MILLISECONDS.toNanos(durationMillis))));
         captor.getValue().run();
     }
 }
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 68d8ebb..b7a1c13 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -18,6 +18,8 @@
 
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
+
 
 import android.annotation.ColorInt;
 import android.annotation.ColorLong;
@@ -39,6 +41,7 @@
 import android.text.SpannableString;
 import android.text.SpannedString;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.text.flags.Flags;
@@ -61,6 +64,7 @@
  * geometries, text and bitmaps.
  */
 public class Paint {
+    private static final String TAG = "Paint";
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private long mNativePaint;
@@ -1803,8 +1807,18 @@
     /**
      * Get the elegant metrics flag.
      *
+     * Note:
+     * For applications target API 35 or later, this function returns true by default.
+     * For applications target API 36 or later, the function call will be ignored and the elegant
+     * text height is always enabled.
+     *
      * @return true if elegant metrics are enabled for text drawing.
+     * @deprecated The underlying UI fonts are deprecated and will be removed from the system image.
+     * Applications supporting scripts with large vertical metrics should adapt their UI by using
+     * fonts designed with corresponding vertical metrics.
      */
+    @Deprecated
+    @FlaggedApi(FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API)
     public boolean isElegantTextHeight() {
         return nGetElegantTextHeight(mNativePaint) != ELEGANT_TEXT_HEIGHT_DISABLED;
     }
@@ -1819,9 +1833,28 @@
      * variants that have not been compacted to fit Latin-based vertical
      * metrics, and also increases top and bottom bounds to provide more space.
      *
+     * <p>
+     * Note:
+     * For applications target API 35 or later, the default value will be true by default.
+     * For applications target API 36 or later, the function call will be ignored and the elegant
+     * text height is always enabled.
+     *
      * @param elegant set the paint's elegant metrics flag for drawing text.
+     * @deprecated This API will be no-op at some point in the future. The underlying UI fonts is
+     * deprecated and will be removed from the system image. Applications supporting scripts with
+     * large vertical metrics should adapt their UI by using fonts designed with corresponding
+     * vertical metrics.
      */
+    @Deprecated
+    @FlaggedApi(FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API)
     public void setElegantTextHeight(boolean elegant) {
+        if (Flags.deprecateElegantTextHeightApi() && !elegant
+                && CompatChanges.isChangeEnabled(DEPRECATE_UI_FONT_ENFORCE)) {
+            if (!elegant) {
+                Log.w(TAG, "The elegant text height cannot be turned off.");
+            }
+            return;
+        }
         nSetElegantTextHeight(mNativePaint,
                 elegant ? ELEGANT_TEXT_HEIGHT_ENABLED : ELEGANT_TEXT_HEIGHT_DISABLED);
     }
@@ -1839,6 +1872,19 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public static final long DEPRECATE_UI_FONT = 279646685L;
 
+    /**
+     * A change ID for deprecating UI fonts enforced.
+     *
+     * From API 36, the elegant text height will not be able to be overridden and always true if the
+     * app has a target SDK of API 36 or later.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = 36)
+    public static final long DEPRECATE_UI_FONT_ENFORCE = 349519475L;
+
+
     private void resetElegantTextHeight() {
         if (CompatChanges.isChangeEnabled(DEPRECATE_UI_FONT)) {
             nSetElegantTextHeight(mNativePaint, ELEGANT_TEXT_HEIGHT_UNSET);
diff --git a/graphics/java/android/graphics/RuntimeColorFilter.java b/graphics/java/android/graphics/RuntimeColorFilter.java
new file mode 100644
index 0000000..52724ce
--- /dev/null
+++ b/graphics/java/android/graphics/RuntimeColorFilter.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 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 android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.graphics.hwui.flags.Flags;
+
+
+/**
+ * <p>A {@link RuntimeColorFilter} calculates a per-pixel color based on the output of a user
+ *  * defined Android Graphics Shading Language (AGSL) function.</p>
+ *
+ * <p>This AGSL function takes in an input color to be operated on. This color is in sRGB and the
+ *  * output is also interpreted as sRGB. The AGSL function signature expects a single input
+ *  * of color (packed as a half4 or float4 or vec4).</p>
+ *
+ * <pre class="prettyprint">
+ * vec4 main(half4 in_color);
+ * </pre>
+ */
+@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS)
+public class RuntimeColorFilter extends ColorFilter {
+
+    private String mAgsl;
+
+    /**
+     * Creates a new RuntimeColorFilter.
+     *
+     * @param agsl The text of AGSL color filter program to run.
+     */
+    public RuntimeColorFilter(@NonNull String agsl) {
+        if (agsl == null) {
+            throw new NullPointerException("RuntimeColorFilter requires a non-null AGSL string");
+        }
+        mAgsl = agsl;
+        // call to parent class to register native RuntimeColorFilter
+        // TODO: find way to get super class to create native instance without requiring the storage
+        // of agsl string
+        getNativeInstance();
+
+    }
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorInt int color) {
+        setUniform(uniformName, Color.valueOf(color).getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorLong long color) {
+        Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
+        if (color == null) {
+            throw new NullPointerException("The color parameter must not be null");
+        }
+        Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float or
+     * float[1] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value) {
+        setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec2 or
+     * float[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
+        setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec3 or
+     * float[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3) {
+        setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
+
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec4 or
+     * float[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4) {
+        setFloatUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float
+     * (for N=1), vecN, or float[N] where N is the length of the values param then an
+     * IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
+        setUniform(uniformName, values, false);
+    }
+
+    private void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(getNativeInstance(), uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(getNativeInstance(), uniformName, values, isColor);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int or int[1]
+     * then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value) {
+        setIntUniform(uniformName, value, 0, 0, 0, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec2 or
+     * int[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
+        setIntUniform(uniformName, value1, value2, 0, 0, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec3 or
+     * int[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
+        setIntUniform(uniformName, value1, value2, value3, 0, 3);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec4 or
+     * int[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2,
+            int value3, int value4) {
+        setIntUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int (for N=1),
+     * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(getNativeInstance(), uniformName, values);
+    }
+
+    private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3,
+            int value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(getNativeInstance(), uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    /**
+     * Assigns the uniform shader to the provided shader parameter.  If the shader program does not
+     * have a uniform shader with that name then an IllegalArgumentException is thrown.
+     *
+     * @param shaderName name matching the uniform declared in the AGSL program
+     * @param shader shader passed into the AGSL program for sampling
+     */
+    public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) {
+        if (shaderName == null) {
+            throw new NullPointerException("The shaderName parameter must not be null");
+        }
+        if (shader == null) {
+            throw new NullPointerException("The shader parameter must not be null");
+        }
+        nativeUpdateChild(getNativeInstance(), shaderName, shader.getNativeInstance());
+    }
+
+    /**
+     * Assigns the uniform color filter to the provided color filter parameter.  If the shader
+     * program does not have a uniform color filter with that name then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param filterName name matching the uniform declared in the AGSL program
+     * @param colorFilter filter passed into the AGSL program for sampling
+     */
+    public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) {
+        if (filterName == null) {
+            throw new NullPointerException("The filterName parameter must not be null");
+        }
+        if (colorFilter == null) {
+            throw new NullPointerException("The colorFilter parameter must not be null");
+        }
+        nativeUpdateChild(getNativeInstance(), filterName, colorFilter.getNativeInstance());
+    }
+
+    /** @hide */
+    @Override
+    protected long createNativeInstance() {
+        return nativeCreateRuntimeColorFilter(mAgsl);
+    }
+
+    private static native long nativeCreateRuntimeColorFilter(String agsl);
+    private static native void nativeUpdateUniforms(
+            long colorFilter, String uniformName, float[] uniforms, boolean isColor);
+    private static native void nativeUpdateUniforms(
+            long colorFilter, String uniformName, float value1, float value2, float value3,
+            float value4, int count);
+    private static native void nativeUpdateUniforms(
+            long colorFilter, String uniformName, int[] uniforms);
+    private static native void nativeUpdateUniforms(
+            long colorFilter, String uniformName, int value1, int value2, int value3,
+            int value4, int count);
+    private static native void nativeUpdateChild(long colorFilter, String childName, long child);
+
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index ad194f7..6398c7a2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -39,6 +39,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityThread;
@@ -1394,10 +1395,14 @@
         }
 
         private void showVeils(@NonNull SurfaceControl.Transaction t) {
-            final Color primaryVeilColor = getContainerBackgroundColor(
-                    mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR);
-            final Color secondaryVeilColor = getContainerBackgroundColor(
-                    mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR);
+            final Color primaryVeilColor = getVeilColor(
+                    mProperties.mDividerAttributes.getPrimaryVeilColor(),
+                    mProperties.mPrimaryContainer,
+                    DEFAULT_PRIMARY_VEIL_COLOR);
+            final Color secondaryVeilColor = getVeilColor(
+                    mProperties.mDividerAttributes.getSecondaryVeilColor(),
+                    mProperties.mSecondaryContainer,
+                    DEFAULT_SECONDARY_VEIL_COLOR);
             t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor))
                     .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor))
                     .setLayer(mDividerSurface, DIVIDER_LAYER)
@@ -1444,6 +1449,21 @@
             }
         }
 
+        /**
+         * Returns the veil color.
+         *
+         * If the configured color is not transparent, we use the configured color, otherwise we use
+         * the window background color of the top activity. If the background color of the top
+         * activity is unavailable, the default color is used.
+         */
+        @NonNull
+        private static Color getVeilColor(@ColorInt int configuredColor,
+                @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) {
+            return configuredColor != Color.TRANSPARENT
+                    ? Color.valueOf(configuredColor)
+                    : getContainerBackgroundColor(container, defaultColor);
+        }
+
         private static float[] colorToFloatArray(@NonNull Color color) {
             return new float[]{color.red(), color.green(), color.blue()};
         }
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
index 61c09f2..7f54c75 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
@@ -22,42 +22,6 @@
     default_team: "trendy_team_multitasking_windowing",
 }
 
-android_app {
-    name: "WMShellRobolectricScreenshotTestApp",
-    platform_apis: true,
-    certificate: "platform",
-    static_libs: [
-        "WindowManager-Shell",
-        "platform-screenshot-diff-core",
-        "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
-        "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
-    ],
-    asset_dirs: ["goldens/robolectric"],
-    manifest: "AndroidManifestRobolectric.xml",
-    use_resource_processor: true,
-}
-
-android_robolectric_test {
-    name: "WMShellRobolectricScreenshotTests",
-    instrumentation_for: "WMShellRobolectricScreenshotTestApp",
-    upstream: true,
-    java_resource_dirs: [
-        "robolectric/config",
-    ],
-    srcs: [
-        "src/**/*.kt",
-    ],
-    static_libs: [
-        "junit",
-        "androidx.test.runner",
-        "androidx.test.rules",
-        "androidx.test.ext.junit",
-        "truth",
-        "platform-parametric-runner-lib",
-    ],
-    auto_gen_config: true,
-}
-
 android_test {
     name: "WMShellMultivalentScreenshotTestsOnDevice",
     srcs: [
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 52ce8cb..0b515f5 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -23,14 +23,15 @@
 import android.graphics.Color
 import android.graphics.drawable.Icon
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
-import android.view.IWindowManager
 import android.view.WindowManager
-import android.view.WindowManagerGlobal
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.protolog.ProtoLog
 import com.android.launcher3.icons.BubbleIconFactory
@@ -41,6 +42,7 @@
 import com.android.wm.shell.common.FloatingContentCoordinator
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 import com.android.wm.shell.taskview.TaskView
 import com.android.wm.shell.taskview.TaskViewTaskController
 import com.google.common.truth.Truth.assertThat
@@ -51,9 +53,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import org.mockito.kotlin.verify
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
 import java.util.function.Consumer
@@ -72,7 +72,7 @@
     private lateinit var expandedViewManager: FakeBubbleExpandedViewManager
     private lateinit var bubbleStackView: BubbleStackView
     private lateinit var shellExecutor: ShellExecutor
-    private lateinit var windowManager: IWindowManager
+    private lateinit var windowManager: WindowManager
     private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
     private lateinit var bubbleData: BubbleData
     private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
@@ -83,9 +83,8 @@
         PhysicsAnimatorTestUtils.prepareForTest()
         // Disable protolog tool when running the tests from studio
         ProtoLog.REQUIRE_PROTOLOGTOOL = false
-        windowManager = WindowManagerGlobal.getWindowManagerService()!!
         shellExecutor = TestShellExecutor()
-        val windowManager = context.getSystemService(WindowManager::class.java)
+        windowManager = context.getSystemService(WindowManager::class.java)
         iconFactory =
             BubbleIconFactory(
                 context,
@@ -354,6 +353,16 @@
         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
     }
 
+    @Test
+    fun removeFromWindow_stopMonitoringSwipeUpGesture() {
+        spyOn(bubbleStackView)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            // No way to add to window in the test environment right now so just pretend
+            bubbleStackView.onDetachedFromWindow()
+        }
+        verify(bubbleStackView).stopMonitoringSwipeUpGesture()
+    }
+
     private fun createAndInflateChatBubble(key: String): Bubble {
         val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
         val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
index 62782a7..e7ead63 100644
--- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
@@ -22,14 +22,14 @@
     android:gravity="bottom|end">
 
     <include android:id="@+id/size_compat_hint"
-        android:visibility="gone"
+        android:visibility="invisible"
         android:layout_width="@dimen/compat_hint_width"
         android:layout_height="wrap_content"
         layout="@layout/compat_mode_hint"/>
 
     <ImageButton
         android:id="@+id/size_compat_restart_button"
-        android:visibility="gone"
+        android:visibility="invisible"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginEnd="@dimen/compat_button_margin"
diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml
index 433d854..b5f04c3 100644
--- a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml
@@ -22,14 +22,14 @@
     android:gravity="bottom|end">
 
     <include android:id="@+id/user_aspect_ratio_settings_hint"
-        android:visibility="gone"
+        android:visibility="invisible"
         android:layout_width="@dimen/compat_hint_width"
         android:layout_height="wrap_content"
         layout="@layout/compat_mode_hint"/>
 
     <ImageButton
         android:id="@+id/user_aspect_ratio_settings_button"
-        android:visibility="gone"
+        android:visibility="invisible"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginEnd="@dimen/compat_button_margin"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index ff3dc33..ae0485f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -191,6 +191,16 @@
                 @Override
                 public void onResult(@Nullable Bundle result) {
                     mShellExecutor.execute(() -> {
+                        if (mBackGestureStarted && result != null && result.getBoolean(
+                                BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED)) {
+                            // Host app won't able to process motion event anymore, so pilfer
+                            // pointers anyway.
+                            if (mBackNavigationInfo != null) {
+                                mBackNavigationInfo.disableAppProgressGenerationAllowed();
+                            }
+                            tryPilferPointers();
+                            return;
+                        }
                         if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
                             // If an uninterruptible animation is already in progress, we should
                             // ignore this due to it may cause focus lost. (alpha = 0)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 35a0d07..88f55b8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1704,6 +1704,7 @@
         getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
         getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        stopMonitoringSwipeUpGesture();
     }
 
     @Override
@@ -2313,7 +2314,8 @@
     /**
      * Stop monitoring for swipe up gesture
      */
-    void stopMonitoringSwipeUpGesture() {
+    @VisibleForTesting
+    public void stopMonitoringSwipeUpGesture() {
         stopMonitoringSwipeUpGestureInternal();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
index 688f8ca..49c2785 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
@@ -68,7 +68,7 @@
 
     private void setViewVisibility(@IdRes int resId, boolean show) {
         final View view = findViewById(resId);
-        int visibility = show ? View.VISIBLE : View.GONE;
+        int visibility = show ? View.VISIBLE : View.INVISIBLE;
         if (view.getVisibility() == visibility) {
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
index b141beb..fd1bbc4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
@@ -100,7 +100,7 @@
 
     private void setViewVisibility(@IdRes int resId, boolean show) {
         final View view = findViewById(resId);
-        int visibility = show ? View.VISIBLE : View.GONE;
+        int visibility = show ? View.VISIBLE : View.INVISIBLE;
         if (view.getVisibility() == visibility) {
             return;
         }
@@ -171,7 +171,7 @@
         fadeOut.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                view.setVisibility(View.GONE);
+                view.setVisibility(View.INVISIBLE);
             }
         });
         fadeOut.start();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index f3f91e6e..615dad3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -842,8 +842,8 @@
     @WMSingleton
     @Provides
     static ReturnToDragStartAnimator provideReturnToDragStartAnimator(
-            Context context, InteractionJankMonitor interactionJankMonitor) {
-        return new ReturnToDragStartAnimator(context, interactionJankMonitor);
+            InteractionJankMonitor interactionJankMonitor) {
+        return new ReturnToDragStartAnimator(interactionJankMonitor);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index fdb01ab..fda709a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -23,12 +23,12 @@
 import android.util.ArraySet
 import android.util.SparseArray
 import android.view.Display.INVALID_DISPLAY
+import android.window.DesktopModeFlags
 import android.window.WindowContainerToken
 import androidx.core.util.forEach
 import androidx.core.util.keyIterator
 import androidx.core.util.valueIterator
 import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -294,6 +294,9 @@
                 taskId, isVisible, displayId)
             logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount)
             notifyVisibleTaskListeners(displayId, newCount)
+            if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
+                updatePersistentRepository(displayId)
+            }
         }
     }
 
@@ -341,7 +344,7 @@
         desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId)
         // Unminimize the task if it is minimized.
         unminimizeTask(displayId, taskId)
-        if (Flags.enableDesktopWindowingPersistence()) {
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
             updatePersistentRepository(displayId)
         }
     }
@@ -359,7 +362,7 @@
             desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
         }
         updateTask(displayId, taskId, isVisible = false)
-        if (Flags.enableDesktopWindowingPersistence()) {
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
             updatePersistentRepository(displayId)
         }
     }
@@ -407,7 +410,7 @@
         unminimizeTask(displayId, taskId)
         removeActiveTask(taskId)
         removeVisibleTask(taskId)
-        if (Flags.enableDesktopWindowingPersistence()) {
+        if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
             updatePersistentRepository(displayId)
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 577c18c..515b203d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -54,6 +54,7 @@
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.widget.Toast
 import android.window.DesktopModeFlags
 import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
@@ -877,7 +878,6 @@
                     taskSurface,
                     startBounds = currentDragBounds,
                     endBounds = containerBounds,
-                    isResizable = taskInfo.isResizeable
                 )
             }
             return
@@ -1012,7 +1012,6 @@
                     taskSurface,
                     startBounds = currentDragBounds,
                     endBounds = destinationBounds,
-                    isResizable = taskInfo.isResizeable,
                 )
             }
             return
@@ -1046,7 +1045,13 @@
                 taskSurface,
                 startBounds = currentDragBounds,
                 endBounds = dragStartBounds,
-                isResizable = taskInfo.isResizeable,
+                doOnEnd = {
+                    Toast.makeText(
+                        context,
+                        com.android.wm.shell.R.string.desktop_mode_non_resizable_snap_text,
+                        Toast.LENGTH_SHORT
+                    ).show()
+                },
             )
         } else {
             val resizeTrigger = if (position == SnapPosition.LEFT) {
@@ -1156,7 +1161,7 @@
                 if (runningTaskInfo != null) {
                     // Task is already running, reorder it to the front
                     wct.reorder(runningTaskInfo.token, /* onTop= */ true)
-                } else if (Flags.enableDesktopWindowingPersistence()) {
+                } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
                     // Task is not running, start it
                     wct.startTask(
                         taskId,
@@ -1984,17 +1989,32 @@
                 )
             }
             IndicatorType.NO_INDICATOR -> {
+                // Create a copy so that we can animate from the current bounds if we end up having
+                // to snap the surface back without a WCT change.
+                val destinationBounds = Rect(currentDragBounds)
                 // If task bounds are outside valid drag area, snap them inward
                 DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
-                    currentDragBounds,
+                    destinationBounds,
                     validDragArea
                 )
 
-                if (currentDragBounds == dragStartBounds) return
+                if (destinationBounds == dragStartBounds) {
+                    // There's no actual difference between the start and end bounds, so while a
+                    // WCT change isn't needed, the dragged surface still needs to be snapped back
+                    // to its original location.
+                    releaseVisualIndicator()
+                    returnToDragStartAnimator.start(
+                        taskInfo.taskId,
+                        taskSurface,
+                        startBounds = currentDragBounds,
+                        endBounds = dragStartBounds,
+                    )
+                    return
+                }
 
                 // Update task bounds so that the task position will match the position of its leash
                 val wct = WindowContainerTransaction()
-                wct.setBounds(taskInfo.token, currentDragBounds)
+                wct.setBounds(taskInfo.token, destinationBounds)
                 transitions.startTransition(TRANSIT_CHANGE, wct, null)
 
                 releaseVisualIndicator()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index dedd44f..d537da8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -60,7 +60,8 @@
  * entering and exiting freeform.
  */
 public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
-    private static final int FULLSCREEN_ANIMATION_DURATION = 336;
+    @VisibleForTesting
+    static final int FULLSCREEN_ANIMATION_DURATION = 336;
 
     private final Context mContext;
     private final Transitions mTransitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
index f4df42c..4e08d10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt
@@ -19,28 +19,24 @@
 import android.animation.Animator
 import android.animation.RectEvaluator
 import android.animation.ValueAnimator
-import android.content.Context
 import android.graphics.Rect
 import android.view.SurfaceControl
-import android.widget.Toast
 import androidx.core.animation.addListener
 import com.android.internal.jank.Cuj
 import com.android.internal.jank.InteractionJankMonitor
-import com.android.wm.shell.R
 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
 import java.util.function.Supplier
 
 /** Animates the task surface moving from its current drag position to its pre-drag position. */
 class ReturnToDragStartAnimator(
-    private val context: Context,
     private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
     private val interactionJankMonitor: InteractionJankMonitor
 ) {
     private var boundsAnimator: Animator? = null
     private lateinit var taskRepositionAnimationListener: OnTaskRepositionAnimationListener
 
-    constructor(context: Context, interactionJankMonitor: InteractionJankMonitor) :
-            this(context, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor)
+    constructor(interactionJankMonitor: InteractionJankMonitor) :
+            this(Supplier { SurfaceControl.Transaction() }, interactionJankMonitor)
 
     /** Sets a listener for the start and end of the reposition animation. */
     fun setTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -53,7 +49,7 @@
         taskSurface: SurfaceControl,
         startBounds: Rect,
         endBounds: Rect,
-        isResizable: Boolean
+        doOnEnd: (() -> Unit)? = null,
     ) {
         val tx = transactionSupplier.get()
 
@@ -87,13 +83,7 @@
                                 .apply()
                             taskRepositionAnimationListener.onAnimationEnd(taskId)
                             boundsAnimator = null
-                            if (!isResizable) {
-                                Toast.makeText(
-                                    context,
-                                    R.string.desktop_mode_non_resizable_snap_text,
-                                    Toast.LENGTH_SHORT
-                                ).show()
-                            }
+                            doOnEnd?.invoke()
                             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE)
                         }
                     )
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index 2d11e02..9e646f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -50,7 +50,9 @@
         DataStoreFactory.create(
             serializer = DesktopPersistentRepositoriesSerializer,
             produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) },
-            scope = bgCoroutineScope))
+            scope = bgCoroutineScope,
+        ),
+    )
 
     /** Provides `dataStore.data` flow and handles exceptions thrown during collection */
     private val dataStoreFlow: Flow<DesktopPersistentRepositories> =
@@ -116,7 +118,11 @@
                 val desktop =
                     getDesktop(currentRepository, desktopId)
                         .toBuilder()
-                        .updateTaskStates(visibleTasks, minimizedTasks)
+                        .updateTaskStates(
+                            visibleTasks,
+                            minimizedTasks,
+                            freeformTasksInZOrder,
+                        )
                         .updateZOrder(freeformTasksInZOrder)
 
                 desktopPersistentRepositories
@@ -169,9 +175,21 @@
 
         private fun Desktop.Builder.updateTaskStates(
             visibleTasks: ArraySet<Int>,
-            minimizedTasks: ArraySet<Int>
+            minimizedTasks: ArraySet<Int>,
+            freeformTasksInZOrder: ArrayList<Int>,
         ): Desktop.Builder {
             clearTasksByTaskId()
+
+            // Handle the case where tasks are not marked as visible but are meant to be visible
+            // after reboot. E.g. User moves out of desktop when there are multiple tasks are
+            // visible, they will be marked as not visible afterwards. This ensures that they are
+            // still persisted as visible.
+            // TODO - b/350476823: Remove this logic once repository holds expanded tasks
+            if (freeformTasksInZOrder.size > visibleTasks.size + minimizedTasks.size &&
+                visibleTasks.isEmpty()
+            ) {
+                visibleTasks.addAll(freeformTasksInZOrder.filterNot { it in minimizedTasks })
+            }
             putAllTasksByTaskId(
                 visibleTasks.associateWith {
                     createDesktopTask(it, state = DesktopTaskState.VISIBLE)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index fc4ed15..d815656 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -17,7 +17,7 @@
 package com.android.wm.shell.desktopmode.persistence
 
 import android.content.Context
-import com.android.window.flags.Flags
+import android.window.DesktopModeFlags
 import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
@@ -36,7 +36,7 @@
     @ShellMainThread private val mainCoroutineScope: CoroutineScope,
 ) : DesktopRepositoryInitializer {
     override fun initialize(repository: DesktopRepository) {
-        if (!Flags.enableDesktopWindowingPersistence()) return
+        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return
         //  TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized
         mainCoroutineScope.launch {
             val desktop = persistentRepository.readDesktop() ?: return@launch
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
index a93ef12..3f9b0c3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.pip2.animation;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.content.Context;
@@ -27,6 +28,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.R;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.shared.animation.Interpolators;
@@ -34,8 +36,7 @@
 /**
  * Animator that handles bounds animations for exit-via-expanding PIP.
  */
-public class PipExpandAnimator extends ValueAnimator
-        implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
+public class PipExpandAnimator extends ValueAnimator {
     @NonNull
     private final SurfaceControl mLeash;
     private final SurfaceControl.Transaction mStartTransaction;
@@ -58,12 +59,61 @@
     // Bounds updated by the evaluator as animator is running.
     private final Rect mAnimatedRect = new Rect();
 
-    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+    private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
             mSurfaceControlTransactionFactory;
     private final RectEvaluator mRectEvaluator;
     private final RectEvaluator mInsetEvaluator;
     private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
 
+    private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationStart(Animator animation) {
+            super.onAnimationStart(animation);
+            if (mAnimationStartCallback != null) {
+                mAnimationStartCallback.run();
+            }
+            if (mStartTransaction != null) {
+                mStartTransaction.apply();
+            }
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            super.onAnimationEnd(animation);
+            if (mFinishTransaction != null) {
+                // finishTransaction might override some state (eg. corner radii) so we want to
+                // manually set the state to the end of the animation
+                mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash,
+                                mSourceRectHint, mBaseBounds, mAnimatedRect, getInsets(1f),
+                                false /* isInPipDirection */, 1f)
+                        .round(mFinishTransaction, mLeash, false /* applyCornerRadius */)
+                        .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */);
+            }
+            if (mAnimationEndCallback != null) {
+                mAnimationEndCallback.run();
+            }
+        }
+    };
+
+    private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener =
+            new AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+                    final SurfaceControl.Transaction tx =
+                            mSurfaceControlTransactionFactory.getTransaction();
+                    final float fraction = getAnimatedFraction();
+
+                    // TODO (b/350801661): implement fixed rotation
+                    Rect insets = getInsets(fraction);
+                    mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint,
+                                    mBaseBounds, mAnimatedRect,
+                                    insets, false /* isInPipDirection */, fraction)
+                            .round(tx, mLeash, false /* applyCornerRadius */)
+                            .shadow(tx, mLeash, false /* applyCornerRadius */);
+                    tx.apply();
+                }
+            };
+
     public PipExpandAnimator(Context context,
             @NonNull SurfaceControl leash,
             SurfaceControl.Transaction startTransaction,
@@ -105,8 +155,8 @@
         setObjectValues(startBounds, endBounds);
         setEvaluator(mRectEvaluator);
         setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        addListener(this);
-        addUpdateListener(this);
+        addListener(mAnimatorListener);
+        addUpdateListener(mAnimatorUpdateListener);
     }
 
     public void setAnimationStartCallback(@NonNull Runnable runnable) {
@@ -117,58 +167,15 @@
         mAnimationEndCallback = runnable;
     }
 
-    @Override
-    public void onAnimationStart(@NonNull Animator animation) {
-        if (mAnimationStartCallback != null) {
-            mAnimationStartCallback.run();
-        }
-        if (mStartTransaction != null) {
-            mStartTransaction.apply();
-        }
-    }
-
-    @Override
-    public void onAnimationEnd(@NonNull Animator animation) {
-        if (mFinishTransaction != null) {
-            // finishTransaction might override some state (eg. corner radii) so we want to
-            // manually set the state to the end of the animation
-            mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint,
-                            mBaseBounds, mAnimatedRect, getInsets(1f),
-                            false /* isInPipDirection */, 1f)
-                    .round(mFinishTransaction, mLeash, false /* applyCornerRadius */)
-                    .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */);
-        }
-        if (mAnimationEndCallback != null) {
-            mAnimationEndCallback.run();
-        }
-    }
-
-    @Override
-    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
-        final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
-        final float fraction = getAnimatedFraction();
-
-        // TODO (b/350801661): implement fixed rotation
-
-        Rect insets = getInsets(fraction);
-        mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint,
-                        mBaseBounds, mAnimatedRect, insets, false /* isInPipDirection */, fraction)
-                .round(tx, mLeash, false /* applyCornerRadius */)
-                .shadow(tx, mLeash, false /* applyCornerRadius */);
-        tx.apply();
-    }
-
     private Rect getInsets(float fraction) {
         final Rect startInsets = mSourceRectHintInsets;
         final Rect endInsets = mZeroInsets;
         return mInsetEvaluator.evaluate(fraction, startInsets, endInsets);
     }
 
-    // no-ops
-
-    @Override
-    public void onAnimationCancel(@NonNull Animator animation) {}
-
-    @Override
-    public void onAnimationRepeat(@NonNull Animator animation) {}
+    @VisibleForTesting
+    void setSurfaceControlTransactionFactory(
+            @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
+        mSurfaceControlTransactionFactory = factory;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 245829e..371bdd5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -45,4 +45,7 @@
 
     /** A task has moved to front. */
     oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo);
+
+    /** A task info has changed. */
+    oneway void onTaskInfoChanged(in RunningTaskInfo taskInfo);
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 6086801..faa2015 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -47,7 +47,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.protolog.ProtoLog;
-import com.android.window.flags.Flags;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
@@ -289,6 +288,11 @@
     }
 
     @Override
+    public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+        notifyTaskInfoChanged(taskInfo);
+    }
+
+    @Override
     public void onTaskMovedToFrontThroughTransition(
             ActivityManager.RunningTaskInfo runningTaskInfo) {
         notifyTaskMovedToFront(runningTaskInfo);
@@ -355,6 +359,19 @@
         }
     }
 
+    private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (mListener == null
+                || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
+                || taskInfo.realActivity == null) {
+            return;
+        }
+        try {
+            mListener.onTaskInfoChanged(taskInfo);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed call onTaskInfoChanged", e);
+        }
+    }
+
     private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
@@ -426,7 +443,7 @@
                 // If task has their app bounds set to null which happens after reboot, set the
                 // app bounds to persisted lastFullscreenBounds. Also set the position in parent
                 // to the top left of the bounds.
-                if (Flags.enableDesktopWindowingPersistence()
+                if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()
                         && taskInfo.configuration.windowConfiguration.getAppBounds() == null) {
                     taskInfo.configuration.windowConfiguration.setAppBounds(
                             taskInfo.lastNonFullscreenBounds);
@@ -636,6 +653,11 @@
             public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onTaskMovedToFront(taskInfo));
             }
+
+            @Override
+            public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+                mListener.call(l -> l.onTaskInfoChanged(taskInfo));
+            }
         };
 
         public IRecentTasksImpl(RecentTasksController controller) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
index 1af99f9..d28a462 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -20,8 +20,9 @@
 import android.os.IBinder
 import android.util.ArrayMap
 import android.view.SurfaceControl
-import android.window.TransitionInfo
+import android.view.WindowManager.TRANSIT_CHANGE
 import android.window.DesktopModeFlags
+import android.window.TransitionInfo
 import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
@@ -69,8 +70,10 @@
                 // Find the first task that is opening, this should be the one at the front after
                 // the transition
                 if (TransitionUtil.isOpeningType(change.mode)) {
-                    notifyTaskStackTransitionObserverListeners(taskInfo)
+                    notifyOnTaskMovedToFront(taskInfo)
                     break
+                } else if (change.mode == TRANSIT_CHANGE) {
+                    notifyOnTaskChanged(taskInfo)
                 }
             }
         }
@@ -95,15 +98,23 @@
         taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener)
     }
 
-    private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) {
+    private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) {
         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
             executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
         }
     }
 
+    private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) {
+        taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+            executor.execute { listener.onTaskChangedThroughTransition(taskInfo) }
+        }
+    }
+
     /** Listener to use to get updates regarding task stack from this observer */
     interface TaskStackTransitionObserverListener {
         /** Called when a task is moved to front. */
         fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+        /** Called when a task info has changed. */
+        fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {}
     }
 }
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 a3324cc6..29b8ddd 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
@@ -130,7 +130,6 @@
 import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
-import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
 import com.android.wm.shell.windowdecor.extension.InsetsStateKt;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
@@ -181,7 +180,6 @@
     private boolean mTransitionDragActive;
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
-    private DesktopStatusBarInputLayerSupplier mStatusBarInputLayerSupplier;
 
     private final ExclusionRegionListener mExclusionRegionListener =
             new ExclusionRegionListenerImpl();
@@ -420,11 +418,7 @@
                         return Unit.INSTANCE;
                     });
         }
-        if (Flags.enableHandleInputFix()) {
-            mStatusBarInputLayerSupplier =
-                    new DesktopStatusBarInputLayerSupplier(mContext, mMainHandler);
-            mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
-        }
+        mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
     }
 
     @Override
@@ -480,7 +474,6 @@
             removeTaskFromEventReceiver(oldTaskInfo.displayId);
             incrementEventReceiverTasks(taskInfo.displayId);
         }
-        decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo));
         decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
         mActivityOrientationChangeHandler.ifPresent(handler ->
                 handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
@@ -519,7 +512,6 @@
         if (decoration == null) {
             createWindowDecoration(taskInfo, taskSurface, startT, finishT);
         } else {
-            decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo));
             decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                     false /* shouldSetTaskPositionAndCrop */,
                     mFocusTransitionObserver.hasGlobalFocus(taskInfo));
@@ -673,7 +665,7 @@
         decoration.closeHandleMenu();
         // When the app enters split-select, the handle will no longer be visible, meaning
         // we shouldn't receive input for it any longer.
-        decoration.detachStatusBarInputLayer();
+        decoration.disposeStatusBarInputLayer();
         mDesktopTasksController.requestSplit(decoration.mTaskInfo, false /* leftOrTop */);
     }
 
@@ -1314,8 +1306,8 @@
                         // should not be receiving any input.
                         if (resultType == TO_SPLIT_LEFT_INDICATOR
                                 || resultType == TO_SPLIT_RIGHT_INDICATOR) {
-                            relevantDecor.detachStatusBarInputLayer();
-                            // We should also detach the other split task's input layer if
+                            relevantDecor.disposeStatusBarInputLayer();
+                            // We should also dispose the other split task's input layer if
                             // applicable.
                             final int splitPosition = mSplitScreenController
                                     .getSplitPosition(relevantDecor.mTaskInfo.taskId);
@@ -1328,7 +1320,7 @@
                                         mSplitScreenController.getTaskInfo(oppositePosition);
                                 if (oppositeTaskInfo != null) {
                                     mWindowDecorByTaskId.get(oppositeTaskInfo.taskId)
-                                            .detachStatusBarInputLayer();
+                                            .disposeStatusBarInputLayer();
                                 }
                             }
                         }
@@ -1578,7 +1570,6 @@
                 touchEventListener, touchEventListener, touchEventListener, touchEventListener);
         windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
         windowDecoration.setDragPositioningCallback(taskPositioner);
-        windowDecoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo));
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,
                 mFocusTransitionObserver.hasGlobalFocus(taskInfo));
@@ -1587,18 +1578,6 @@
         }
     }
 
-    /** Decide which cached status bar input layer should be used for a decoration. */
-    private AdditionalSystemViewContainer getStatusBarInputLayer(
-            RunningTaskInfo taskInfo
-    ) {
-        if (mStatusBarInputLayerSupplier == null) return null;
-        return mStatusBarInputLayerSupplier.getStatusBarInputLayer(
-                taskInfo,
-                mSplitScreenController.getSplitPosition(taskInfo.taskId),
-                mSplitScreenController.isLeftRightSplit()
-        );
-    }
-
     private RunningTaskInfo getOtherSplitTask(int taskId) {
         @SplitPosition int remainingTaskPosition = mSplitScreenController
                 .getSplitPosition(taskId) == SPLIT_POSITION_BOTTOM_OR_RIGHT
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 9f37358..f930748 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
@@ -106,7 +106,6 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer;
 import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
@@ -206,7 +205,6 @@
     private final MultiInstanceHelper mMultiInstanceHelper;
     private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
     private final DesktopRepository mDesktopRepository;
-    private AdditionalSystemViewContainer mStatusBarInputLayer;
 
     DesktopModeWindowDecoration(
             Context context,
@@ -550,13 +548,13 @@
                 notifyNoCaptionHandle();
             }
             mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
-            detachStatusBarInputLayer();
+            disposeStatusBarInputLayer();
             Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
             return;
         }
 
         if (oldRootView != mResult.mRootView) {
-            detachStatusBarInputLayer();
+            disposeStatusBarInputLayer();
             mWindowDecorViewHolder = createViewHolder();
         }
         Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
@@ -574,9 +572,6 @@
                     mTaskInfo, position, mResult.mCaptionWidth, mResult.mCaptionHeight,
                     isCaptionVisible()
             ));
-            if (mStatusBarInputLayer != null) {
-                asAppHandle(mWindowDecorViewHolder).bindStatusBarInputLayer(mStatusBarInputLayer);
-            }
         } else {
             mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData(
                     mTaskInfo,
@@ -790,15 +785,15 @@
     }
 
     /**
-     * Detach the status bar input layer from this decoration. Intended to be
+     * Dispose of the view used to forward inputs in status bar region. Intended to be
      * used any time handle is no longer visible.
      */
-    void detachStatusBarInputLayer() {
+    void disposeStatusBarInputLayer() {
         if (!isAppHandle(mWindowDecorViewHolder)
                 || !Flags.enableHandleInputFix()) {
             return;
         }
-        asAppHandle(mWindowDecorViewHolder).detachStatusBarInputLayer();
+        asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer();
     }
 
     private WindowDecorationViewHolder createViewHolder() {
@@ -1638,7 +1633,7 @@
         closeManageWindowsMenu();
         mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
         disposeResizeVeil();
-        detachStatusBarInputLayer();
+        disposeStatusBarInputLayer();
         clearCurrentViewHostRunnable();
         if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
             notifyNoCaptionHandle();
@@ -1755,16 +1750,6 @@
                 + "}";
     }
 
-    /**
-     * Set the view container to be used to forward input through status bar. Null in cases
-     * where input forwarding isn't needed.
-     */
-    public void setStatusBarInputLayer(
-            @Nullable AdditionalSystemViewContainer additionalSystemViewContainer
-    ) {
-        mStatusBarInputLayer = additionalSystemViewContainer;
-    }
-
     static class Factory {
 
         DesktopModeWindowDecoration create(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt
deleted file mode 100644
index 9c5215d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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.wm.shell.windowdecor
-
-import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration
-import android.content.Context
-import android.graphics.PixelFormat
-import android.os.Handler
-import android.view.Gravity
-import android.view.View
-import android.view.WindowManager
-import com.android.wm.shell.shared.annotations.ShellMainThread
-import com.android.wm.shell.shared.split.SplitScreenConstants
-import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
-
-/**
- * Supplier for [AdditionalSystemViewContainer] objects to be used for forwarding input
- * events through status bar to an app handle. Currently supports two simultaneous input layers.
- *
- * The supplier will pick one of two input layer view containers to use: one for tasks in
- * fullscreen or top/left split stage, and one for tasks in right split stage.
- */
-class DesktopStatusBarInputLayerSupplier(
-    private val context: Context,
-    @ShellMainThread handler: Handler
-) {
-    private val inputLayers: MutableList<AdditionalSystemViewContainer> = mutableListOf()
-
-    init {
-        // Post this as creation of the input layer views is a relatively expensive operation.
-        handler.post {
-            repeat(TOTAL_INPUT_LAYERS) {
-                inputLayers.add(createInputLayer())
-            }
-        }
-    }
-
-    private fun createInputLayer(): AdditionalSystemViewContainer {
-        val lp = WindowManager.LayoutParams(
-            WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
-            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
-            PixelFormat.TRANSPARENT
-        )
-        lp.title = "Desktop status bar input layer"
-        lp.gravity = Gravity.LEFT or Gravity.TOP
-        lp.setTrustedOverlay()
-
-        // Make this window a spy window to enable it to pilfer pointers from the system-wide
-        // gesture listener that receives events before window. This is to prevent notification
-        // shade gesture when we swipe down to enter desktop.
-        lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
-        lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-        val view = View(context)
-        view.visibility = View.GONE
-        return AdditionalSystemViewContainer(
-            WindowManagerWrapper(
-                context.getSystemService<WindowManager>(WindowManager::class.java)
-            ),
-            view,
-            lp
-        )
-    }
-
-    /**
-     * Decide which cached status bar input layer should be used for a decoration, if any.
-     *
-     * [splitPosition] and [isLeftRightSplit] are used to determine which input layer we use.
-     * The first one is reserved for fullscreen tasks or tasks in top/left split,
-     * while the second one is exclusively used for tasks in right split stage. Note we care about
-     * left-right vs top-bottom split as the bottom stage should not use an input layer.
-     */
-    fun getStatusBarInputLayer(
-        taskInfo: RunningTaskInfo,
-        @SplitScreenConstants.SplitPosition splitPosition: Int,
-        isLeftRightSplit: Boolean
-    ): AdditionalSystemViewContainer? {
-        if (!taskInfo.isVisibleRequested) return null
-        // Fullscreen and top/left split tasks will use the first input layer.
-        if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN
-            || splitPosition == SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
-        ) {
-            return inputLayers[LEFT_TOP_INPUT_LAYER]
-        }
-        // Right split tasks will use the second one.
-        if (isLeftRightSplit && splitPosition == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
-        ) {
-            return inputLayers[RIGHT_SPLIT_INPUT_LAYER]
-        }
-        // Which leaves bottom split and freeform tasks, which do not need an input layer
-        // as the status bar is not blocking them.
-        return null
-    }
-
-    companion object {
-        private const val TOTAL_INPUT_LAYERS = 2
-        // Input layer index for fullscreen tasks and tasks in top-left split
-        private const val LEFT_TOP_INPUT_LAYER = 0
-        // Input layer index for tasks in right split stage. Does not include bottom split as that
-        // stage is not blocked by status bar.
-        private const val RIGHT_SPLIT_INPUT_LAYER = 1
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 1451f36..8b6aaaf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -23,8 +23,8 @@
 import android.view.LayoutInflater
 import android.view.SurfaceControl
 import android.view.View
+import android.view.WindowInsets
 import android.view.WindowManager
-import android.view.WindowManager.LayoutParams
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
 
 /**
@@ -33,11 +33,27 @@
  */
 class AdditionalSystemViewContainer(
     private val windowManagerWrapper: WindowManagerWrapper,
-    override val view: View,
-    val lp: LayoutParams
+    taskId: Int,
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int,
+    flags: Int,
+    @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0,
+    override val view: View
 ) : AdditionalViewContainer() {
+    val lp: WindowManager.LayoutParams = WindowManager.LayoutParams(
+        width, height, x, y,
+        WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
+        flags,
+        PixelFormat.TRANSPARENT
+    ).apply {
+        title = "Additional view container of Task=$taskId"
+        gravity = Gravity.LEFT or Gravity.TOP
+        setTrustedOverlay()
+        this.forciblyShownTypes = forciblyShownTypes
+    }
 
-    /** Provide a layout id of a view to inflate for this view container. */
     constructor(
         context: Context,
         windowManagerWrapper: WindowManagerWrapper,
@@ -50,30 +66,15 @@
         @LayoutRes layoutId: Int
     ) : this(
         windowManagerWrapper = windowManagerWrapper,
-        view = LayoutInflater.from(context).inflate(layoutId, null /* parent */),
-        lp = createLayoutParams(x, y, width, height, flags, taskId)
+        taskId = taskId,
+        x = x,
+        y = y,
+        width = width,
+        height = height,
+        flags = flags,
+        view = LayoutInflater.from(context).inflate(layoutId, null /* parent */)
     )
 
-    /** Provide a view directly for this view container */
-    constructor(
-        windowManagerWrapper: WindowManagerWrapper,
-        taskId: Int,
-        x: Int,
-        y: Int,
-        width: Int,
-        height: Int,
-        flags: Int,
-        view: View,
-        forciblyShownTypes: Int = 0
-    ) : this(
-        windowManagerWrapper = windowManagerWrapper,
-        view = view,
-        lp = createLayoutParams(x, y, width, height, flags, taskId).apply {
-            this.forciblyShownTypes = forciblyShownTypes
-        }
-    )
-
-    /** Do not supply a view at all, instead creating the view container with a basic view. */
     constructor(
         context: Context,
         windowManagerWrapper: WindowManagerWrapper,
@@ -85,7 +86,12 @@
         flags: Int
     ) : this(
         windowManagerWrapper = windowManagerWrapper,
-        lp = createLayoutParams(x, y, width, height, flags, taskId),
+        taskId = taskId,
+        x = x,
+        y = y,
+        width = width,
+        height = height,
+        flags = flags,
         view = View(context)
     )
 
@@ -98,7 +104,7 @@
     }
 
     override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) {
-        lp.apply {
+        val lp = (view.layoutParams as WindowManager.LayoutParams).apply {
             this.x = x.toInt()
             this.y = y.toInt()
         }
@@ -118,29 +124,13 @@
         ): AdditionalSystemViewContainer =
             AdditionalSystemViewContainer(
                 windowManagerWrapper = windowManagerWrapper,
-                view = view,
-                lp = createLayoutParams(x, y, width, height, flags, taskId)
+                taskId = taskId,
+                x = x,
+                y = y,
+                width = width,
+                height = height,
+                flags = flags,
+                view = view
             )
     }
-    companion object {
-        fun createLayoutParams(
-            x: Int,
-            y: Int,
-            width: Int,
-            height: Int,
-            flags: Int,
-            taskId: Int
-        ): LayoutParams {
-            return LayoutParams(
-                width, height, x, y,
-                LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
-                flags,
-                PixelFormat.TRANSPARENT
-            ).apply {
-                title = "Additional view container of Task=$taskId"
-                gravity = Gravity.LEFT or Gravity.TOP
-                setTrustedOverlay()
-            }
-        }
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
index 2303ec9..a7087ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -127,7 +127,6 @@
                     resizeMetadata.getLeash(),
                     startBounds = currentBounds,
                     endBounds = destinationBounds,
-                    isResizable = taskInfo.isResizeable,
                 )
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index c4e4946..b5700ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -36,10 +36,13 @@
 import android.widget.ImageButton
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
+import com.android.internal.policy.SystemBarUtils
+import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.shared.animation.Interpolators
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
 
 /**
  * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
@@ -66,12 +69,10 @@
     ) : Data()
 
     private lateinit var taskInfo: RunningTaskInfo
-    private val position: Point = Point()
-    private var width: Int = 0
-    private var height: Int = 0
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
     private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
     private val inputManager = context.getSystemService(InputManager::class.java)
+    private var statusBarInputLayerExists = false
 
     // An invisible View that takes up the same coordinates as captionHandle but is layered
     // above the status bar. The purpose of this View is to receive input intended for
@@ -111,54 +112,21 @@
     ) {
         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
         this.taskInfo = taskInfo
-        this.position.set(position)
-        this.width = width
-        this.height = height
-        if (!isCaptionVisible && statusBarInputLayer != null) {
-            detachStatusBarInputLayer()
+        // If handle is not in status bar region(i.e., bottom stage in vertical split),
+        // do not create an input layer
+        if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return
+        if (!isCaptionVisible && statusBarInputLayerExists) {
+            disposeStatusBarInputLayer()
             return
         }
-    }
-
-    fun bindStatusBarInputLayer(
-        statusBarLayer: AdditionalSystemViewContainer
-    ) {
-        // Input layer view modification takes a significant amount of time;
+        // Input layer view creation / modification takes a significant amount of time;
         // post them so we don't hold up DesktopModeWindowDecoration#relayout.
-        if (statusBarLayer == statusBarInputLayer) {
+        if (statusBarInputLayerExists) {
             handler.post { updateStatusBarInputLayer(position) }
-            return
-        }
-        // Remove the old input layer when changing to a new one.
-        if (statusBarInputLayer != null) detachStatusBarInputLayer()
-        if (statusBarLayer.view.visibility == View.GONE) {
-            statusBarLayer.view.visibility = View.VISIBLE
-        }
-        statusBarInputLayer = statusBarLayer
-        statusBarInputLayer?.let {
-            inputLayer -> setupAppHandleA11y(inputLayer.view)
-        }
-        handler.post {
-            val view = statusBarInputLayer?.view
-                ?: error("Unable to find statusBarInputLayer View")
-            // Caption handle is located within the status bar region, meaning the
-            // DisplayPolicy will attempt to transfer this input to status bar if it's
-            // a swipe down. Pilfer here to keep the gesture in handle alone.
-            view.setOnTouchListener { v, event ->
-                if (event.actionMasked == ACTION_DOWN) {
-                    inputManager.pilferPointers(v.viewRootImpl.inputToken)
-                }
-                captionHandle.dispatchTouchEvent(event)
-                return@setOnTouchListener true
-            }
-            view.setOnHoverListener { _, event ->
-                captionHandle.onHoverEvent(event)
-            }
-            val lp = statusBarInputLayer?.view?.layoutParams as WindowManager.LayoutParams
-            lp.x = position.x
-            lp.y = position.y
-            lp.width = width
-            lp.height = height
+        } else {
+            // Input layer is created on a delay; prevent multiple from being created.
+            statusBarInputLayerExists = true
+            handler.post { createStatusBarInputLayer(position, width, height) }
         }
     }
 
@@ -170,6 +138,40 @@
         animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
     }
 
+    private fun createStatusBarInputLayer(handlePosition: Point,
+                                          handleWidth: Int,
+                                          handleHeight: Int) {
+        if (!Flags.enableHandleInputFix()) return
+        statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper,
+            taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
+            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+        )
+        val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View")
+        val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " +
+                "LayoutParams")
+        lp.title = "Handle Input Layer of task " + taskInfo.taskId
+        lp.setTrustedOverlay()
+        // Make this window a spy window to enable it to pilfer pointers from the system-wide
+        // gesture listener that receives events before window. This is to prevent notification
+        // shade gesture when we swipe down to enter desktop.
+        lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
+        view.setOnHoverListener { _, event ->
+            captionHandle.onHoverEvent(event)
+        }
+        // Caption handle is located within the status bar region, meaning the
+        // DisplayPolicy will attempt to transfer this input to status bar if it's
+        // a swipe down. Pilfer here to keep the gesture in handle alone.
+        view.setOnTouchListener { v, event ->
+            if (event.actionMasked == ACTION_DOWN) {
+                inputManager.pilferPointers(v.viewRootImpl.inputToken)
+            }
+            captionHandle.dispatchTouchEvent(event)
+            return@setOnTouchListener true
+        }
+        setupAppHandleA11y(view)
+        windowManagerWrapper.updateViewLayout(view, lp)
+    }
+
     private fun setupAppHandleA11y(view: View) {
         view.accessibilityDelegate = object : View.AccessibilityDelegate() {
             override fun onInitializeAccessibilityNodeInfo(
@@ -222,12 +224,15 @@
     }
 
     /**
-     * Remove the input listeners from the input layer and remove it from this view holder.
+     * Remove the input layer from [WindowManager]. Should be used when caption handle
+     * is not visible.
      */
-    fun detachStatusBarInputLayer() {
-        statusBarInputLayer?.view?.setOnTouchListener(null)
-        statusBarInputLayer?.view?.setOnHoverListener(null)
-        statusBarInputLayer = null
+    fun disposeStatusBarInputLayer() {
+        statusBarInputLayerExists = false
+        handler.post {
+            statusBarInputLayer?.releaseView()
+            statusBarInputLayer = null
+        }
     }
 
     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 38d9ab9..414c1a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -53,6 +53,7 @@
 import org.mockito.Mockito.spy
 import org.mockito.kotlin.any
 import org.mockito.kotlin.never
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -227,6 +228,33 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+    fun updateTaskVisibility_multipleTasks_persistsVisibleTasks() =
+        runTest(StandardTestDispatcher()) {
+            repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
+            repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true)
+
+            inOrder(persistentRepository).run {
+                verify(persistentRepository)
+                    .addOrUpdateDesktop(
+                        DEFAULT_USER_ID,
+                        DEFAULT_DESKTOP_ID,
+                        visibleTasks = ArraySet(arrayOf(1)),
+                        minimizedTasks = ArraySet(),
+                        freeformTasksInZOrder = arrayListOf()
+                    )
+                verify(persistentRepository)
+                    .addOrUpdateDesktop(
+                        DEFAULT_USER_ID,
+                        DEFAULT_DESKTOP_ID,
+                        visibleTasks = ArraySet(arrayOf(1, 2)),
+                        minimizedTasks = ArraySet(),
+                        freeformTasksInZOrder = arrayListOf()
+                    )
+            }
+        }
+
+    @Test
     fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
         repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
         repo.addClosingTask(DEFAULT_DISPLAY, 1)
@@ -614,7 +642,7 @@
                         minimizedTasks = ArraySet(),
                         freeformTasksInZOrder = arrayListOf(7, 6, 5)
                     )
-                verify(persistentRepository)
+                verify(persistentRepository, times(2))
                     .addOrUpdateDesktop(
                         DEFAULT_USER_ID,
                         DEFAULT_DESKTOP_ID,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 018f9c2..fc89b0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -3029,7 +3029,7 @@
       eq(mockSurface),
       eq(currentDragBounds),
       eq(STABLE_BOUNDS),
-      eq(true)
+      anyOrNull(),
     )
   }
 
@@ -3344,7 +3344,7 @@
       eq(mockSurface),
       eq(currentDragBounds),
       eq(bounds),
-      eq(true)
+      anyOrNull(),
     )
     verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
       ResizeTrigger.SNAP_LEFT_MENU,
@@ -3401,7 +3401,7 @@
       eq(mockSurface),
       eq(currentDragBounds),
       eq(preDragBounds),
-      eq(false)
+      any(),
     )
     verify(desktopModeEventLogger, never()).logTaskResizingStarted(
       any(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
index fefa933..a82e5e8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
@@ -19,8 +19,6 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 
-import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
-
 import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN;
 
 import static org.junit.Assert.assertTrue;
@@ -28,6 +26,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.animation.AnimatorTestRule;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.WindowConfiguration;
@@ -36,6 +35,8 @@
 import android.graphics.Point;
 import android.os.Handler;
 import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.util.DisplayMetrics;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
@@ -53,17 +54,23 @@
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
 import java.util.function.Supplier;
 
 /** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */
 @SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
 public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase {
 
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
+
     @Mock
     private Transitions mTransitions;
     @Mock
@@ -105,7 +112,7 @@
     }
 
     @Test
-    public void testTransitExitDesktopModeAnimation() throws Throwable {
+    public void testTransitExitDesktopModeAnimation() {
         final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN;
         final int taskId = 1;
         WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -118,21 +125,16 @@
         TransitionInfo.Change change =
                 createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN);
         TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change);
-        ArrayList<Exception> exceptions = new ArrayList<>();
-        runOnUiThread(() -> {
-            try {
-                assertTrue(mExitDesktopTaskTransitionHandler
-                        .startAnimation(mToken, info,
-                                new SurfaceControl.Transaction(),
-                                new SurfaceControl.Transaction(),
-                                mTransitionFinishCallback));
-            } catch (Exception e) {
-                exceptions.add(e);
-            }
-        });
-        if (!exceptions.isEmpty()) {
-            throw exceptions.get(0);
-        }
+
+        final boolean animated = mExitDesktopTaskTransitionHandler
+                .startAnimation(mToken, info,
+                        new SurfaceControl.Transaction(),
+                        new SurfaceControl.Transaction(),
+                        mTransitionFinishCallback);
+        mAnimatorTestRule.advanceTimeBy(
+                ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION);
+
+        assertTrue(animated);
     }
 
     private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 7dbadc9..5419887 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -68,6 +68,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @OptIn(ExperimentalCoroutinesApi::class)
+@Ignore("b/374328725")
 class AppHandleEducationControllerTest : ShellTestCase() {
   @JvmField
   @Rule
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java
new file mode 100644
index 0000000..e19a10a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.wm.shell.pip2.animation;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit test against {@link PipExpandAnimator}.
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class PipExpandAnimatorTest {
+
+    @Mock private Context mMockContext;
+
+    @Mock private Resources mMockResources;
+
+    @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
+
+    @Mock private SurfaceControl.Transaction mMockTransaction;
+
+    @Mock private SurfaceControl.Transaction mMockStartTransaction;
+
+    @Mock private SurfaceControl.Transaction mMockFinishTransaction;
+
+    @Mock private Runnable mMockStartCallback;
+
+    @Mock private Runnable mMockEndCallback;
+
+    private PipExpandAnimator mPipExpandAnimator;
+    private Rect mBaseBounds;
+    private Rect mStartBounds;
+    private Rect mEndBounds;
+    private Rect mSourceRectHint;
+    @Surface.Rotation private int mRotation;
+    private SurfaceControl mTestLeash;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockResources.getInteger(anyInt())).thenReturn(0);
+        when(mMockFactory.getTransaction()).thenReturn(mMockTransaction);
+        // No-op on the mMockTransaction
+        when(mMockTransaction.setAlpha(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockTransaction);
+        when(mMockTransaction.setCrop(any(SurfaceControl.class), any(Rect.class)))
+                .thenReturn(mMockTransaction);
+        when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
+                .thenReturn(mMockTransaction);
+        when(mMockTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockTransaction);
+        when(mMockTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockTransaction);
+        // Do the same for mMockFinishTransaction
+        when(mMockFinishTransaction.setAlpha(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockFinishTransaction);
+        when(mMockFinishTransaction.setCrop(any(SurfaceControl.class), any(Rect.class)))
+                .thenReturn(mMockFinishTransaction);
+        when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
+                .thenReturn(mMockFinishTransaction);
+        when(mMockFinishTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockFinishTransaction);
+        when(mMockFinishTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat()))
+                .thenReturn(mMockFinishTransaction);
+
+        mTestLeash = new SurfaceControl.Builder()
+                .setContainerLayer()
+                .setName("PipExpandAnimatorTest")
+                .setCallsite("PipExpandAnimatorTest")
+                .build();
+    }
+
+    @Test
+    public void setAnimationStartCallback_expand_callbackStartCallback() {
+        mRotation = Surface.ROTATION_0;
+        mBaseBounds = new Rect(0, 0, 1_000, 2_000);
+        mStartBounds = new Rect(500, 1_000, 1_000, 2_000);
+        mEndBounds = new Rect(mBaseBounds);
+        mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash,
+                mMockStartTransaction, mMockFinishTransaction,
+                mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint,
+                mRotation);
+        mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory);
+
+        mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback);
+        mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mPipExpandAnimator.start();
+            mPipExpandAnimator.pause();
+        });
+
+        verify(mMockStartCallback).run();
+        verifyZeroInteractions(mMockEndCallback);
+    }
+
+    @Test
+    public void setAnimationEndCallback_expand_callbackStartAndEndCallback() {
+        mRotation = Surface.ROTATION_0;
+        mBaseBounds = new Rect(0, 0, 1_000, 2_000);
+        mStartBounds = new Rect(500, 1_000, 1_000, 2_000);
+        mEndBounds = new Rect(mBaseBounds);
+        mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash,
+                mMockStartTransaction, mMockFinishTransaction,
+                mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint,
+                mRotation);
+        mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory);
+
+        mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback);
+        mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mPipExpandAnimator.start();
+            mPipExpandAnimator.end();
+        });
+
+        verify(mMockStartCallback).run();
+        verify(mMockEndCallback).run();
+    }
+
+    @Test
+    public void onAnimationEnd_expand_leashIsFullscreen() {
+        mRotation = Surface.ROTATION_0;
+        mBaseBounds = new Rect(0, 0, 1_000, 2_000);
+        mStartBounds = new Rect(500, 1_000, 1_000, 2_000);
+        mEndBounds = new Rect(mBaseBounds);
+        mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash,
+                mMockStartTransaction, mMockFinishTransaction,
+                mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint,
+                mRotation);
+        mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory);
+
+        mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback);
+        mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mPipExpandAnimator.start();
+            clearInvocations(mMockTransaction);
+            mPipExpandAnimator.end();
+        });
+
+        verify(mMockTransaction).setCrop(mTestLeash, mEndBounds);
+        verify(mMockTransaction).setCornerRadius(mTestLeash, 0f);
+        verify(mMockTransaction).setShadowRadius(mTestLeash, 0f);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
index afdb687..efe4fb1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -34,6 +34,7 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.extension.isFullscreen
 import com.google.common.truth.Truth.assertThat
 import dagger.Lazy
 import org.junit.Before
@@ -107,8 +108,8 @@
         callOnTransitionFinished()
         executor.flushAll()
 
-        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
-        assertThat(listener.taskInfoToBeNotified.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
             .isEqualTo(change.taskInfo?.windowingMode)
     }
 
@@ -130,8 +131,8 @@
         callOnTransitionFinished()
         executor.flushAll()
 
-        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(1)
-        assertThat(listener.taskInfoToBeNotified.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
             .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
     }
 
@@ -161,9 +162,9 @@
         callOnTransitionFinished()
         executor.flushAll()
 
-        assertThat(listener.taskInfoToBeNotified.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId)
             .isEqualTo(freeformOpenChange.taskInfo?.taskId)
-        assertThat(listener.taskInfoToBeNotified.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
             .isEqualTo(freeformOpenChange.taskInfo?.windowingMode)
     }
 
@@ -199,9 +200,15 @@
         callOnTransitionFinished()
         executor.flushAll()
 
-        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
-        assertThat(listener.taskInfoToBeNotified.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
             .isEqualTo(change.taskInfo?.windowingMode)
+
+        assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1)
+        with(listener.taskInfoOnTaskChanged.last()) {
+            assertThat(taskId).isEqualTo(mergedChange.taskInfo?.taskId)
+            assertThat(windowingMode).isEqualTo(mergedChange.taskInfo?.windowingMode)
+        }
     }
 
     @Test
@@ -236,18 +243,151 @@
         callOnTransitionFinished()
         executor.flushAll()
 
-        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId)
-        assertThat(listener.taskInfoToBeNotified.windowingMode)
-                .isEqualTo(mergedChange.taskInfo?.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId)
+            .isEqualTo(mergedChange.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
+            .isEqualTo(mergedChange.taskInfo?.windowingMode)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+        val freeformState =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val transitionInfoOpen =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build()
+        callOnTransitionReady(transitionInfoOpen)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId)
+            .isEqualTo(freeformState.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
+            .isEqualTo(freeformState.taskInfo?.windowingMode)
+        assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false)
+
+        // create change transition to update the windowing mode to full screen.
+        val fullscreenState =
+            createChange(
+                WindowManager.TRANSIT_CHANGE,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+            )
+        val transitionInfoChange =
+            TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0)
+                .addChange(fullscreenState)
+                .build()
+
+        callOnTransitionReady(transitionInfoChange)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        // Asserting whether freeformState remains the same as before the change
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId)
+            .isEqualTo(freeformState.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false)
+
+        // Asserting changes
+        assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1)
+        with(listener.taskInfoOnTaskChanged.last()) {
+            assertThat(taskId).isEqualTo(fullscreenState.taskInfo?.taskId)
+            assertThat(isFullscreen).isEqualTo(true)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun singleTransition_withOpenAndChange_onlyOpenIsNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Creating multiple changes to be fired in a single transition
+        val freeformState =
+            createChange(
+                mode = WindowManager.TRANSIT_OPEN,
+                taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val fullscreenState =
+            createChange(
+                mode = WindowManager.TRANSIT_CHANGE,
+                taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+            )
+
+        val transitionInfoWithChanges =
+            TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0)
+                .addChange(freeformState)
+                .addChange(fullscreenState)
+                .build()
+
+        callOnTransitionReady(transitionInfoWithChanges)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoOnTaskMovedToFront.taskId)
+            .isEqualTo(freeformState.taskInfo?.taskId)
+        assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false)
+        assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(0)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        val taskId = 1
+
+        // Creating multiple changes to be fired in a single transition
+        val changes =
+            listOf(
+                    WindowConfiguration.WINDOWING_MODE_FREEFORM,
+                    WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION,
+                    WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+                )
+                .map { change ->
+                    createChange(
+                        mode = WindowManager.TRANSIT_CHANGE,
+                        taskInfo = createTaskInfo(taskId, change)
+                    )
+                }
+
+        val transitionInfoWithChanges =
+            TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0)
+                .apply { changes.forEach { c -> this@apply.addChange(c) } }
+                .build()
+
+        callOnTransitionReady(transitionInfoWithChanges)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(changes.size)
+        changes.forEachIndexed { index, change ->
+            assertThat(listener.taskInfoOnTaskChanged[index].taskId)
+                .isEqualTo(change.taskInfo?.taskId)
+            assertThat(listener.taskInfoOnTaskChanged[index].windowingMode)
+                .isEqualTo(change.taskInfo?.windowingMode)
+        }
     }
 
     class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
-        var taskInfoToBeNotified = ActivityManager.RunningTaskInfo()
+        var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo()
+        var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>()
 
         override fun onTaskMovedToFrontThroughTransition(
             taskInfo: ActivityManager.RunningTaskInfo
         ) {
-            taskInfoToBeNotified = taskInfo
+            taskInfoOnTaskMovedToFront = taskInfo
+        }
+
+        override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) {
+            taskInfoOnTaskChanged += taskInfo
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index c42be7f..36c5be1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -946,7 +946,7 @@
     }
 
     @Test
-    fun testDecor_onClickToSplitScreen_detachesStatusBarInputLayer() {
+    fun testDecor_onClickToSplitScreen_disposesStatusBarInputLayer() {
         val toSplitScreenListenerCaptor = forClass(Function0::class.java)
                 as ArgumentCaptor<Function0<Unit>>
         val decor = createOpenTaskDecoration(
@@ -956,7 +956,7 @@
 
         toSplitScreenListenerCaptor.value.invoke()
 
-        verify(decor).detachStatusBarInputLayer()
+        verify(decor).disposeStatusBarInputLayer()
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 8a2c778..f6fed29 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -49,6 +49,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.kotlin.VerificationKt.times;
 
 import android.app.ActivityManager;
 import android.app.assist.AssistContent;
@@ -849,7 +850,8 @@
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
         spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
 
-        verify(mMockHandler).post(runnableArgument.capture());
+        // Once for view host, the other for the AppHandle input layer.
+        verify(mMockHandler, times(2)).post(runnableArgument.capture());
         runnableArgument.getValue().run();
         verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
     }
@@ -876,7 +878,8 @@
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
         spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
-        verify(mMockHandler).post(runnableArgument.capture());
+        // Once for view host, the other for the AppHandle input layer.
+        verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
         spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
 
@@ -890,7 +893,8 @@
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
         spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
-        verify(mMockHandler).post(runnableArgument.capture());
+        // Once for view host, the other for the AppHandle input layer.
+        verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
         spyWindowDecor.close();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
index 5b0cdc3..2ae2461 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -49,6 +49,7 @@
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Captor
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
@@ -196,7 +197,7 @@
 
         verify(toggleResizeDesktopTaskTransitionHandler, times(1))
             .startTransition(capture(wctCaptor), any())
-        verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any())
+        verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), anyOrNull())
         for (change in wctCaptor.value.changes) {
             val bounds = change.value.configuration.windowConfiguration.bounds
             val leftBounds = getLeftTaskBounds()
diff --git a/libs/androidfw/PngCrunch.cpp b/libs/androidfw/PngCrunch.cpp
index cf3c0ee..e945405 100644
--- a/libs/androidfw/PngCrunch.cpp
+++ b/libs/androidfw/PngCrunch.cpp
@@ -506,8 +506,7 @@
   // Set up the write functions which write to our custom data sources.
   png_set_write_fn(write_ptr, (png_voidp)out, WriteDataToStream, nullptr);
 
-  // We want small files and can take the performance hit to achieve this goal.
-  png_set_compression_level(write_ptr, Z_BEST_COMPRESSION);
+  png_set_compression_level(write_ptr, options.compression_level);
 
   // Begin analysis of the image data.
   // Scan the entire image and determine if:
diff --git a/libs/androidfw/include/androidfw/Png.h b/libs/androidfw/include/androidfw/Png.h
index 2ece43e..72be59b 100644
--- a/libs/androidfw/include/androidfw/Png.h
+++ b/libs/androidfw/include/androidfw/Png.h
@@ -31,6 +31,8 @@
 
 struct PngOptions {
   int grayscale_tolerance = 0;
+  // By default we want small files and can take the performance hit to achieve this goal.
+  int compression_level = 9;
 };
 
 /**
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 266c236..fcb7efc 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -384,6 +384,7 @@
         "jni/ScopedParcel.cpp",
         "jni/Shader.cpp",
         "jni/RenderEffect.cpp",
+        "jni/RuntimeEffectUtils.cpp",
         "jni/Typeface.cpp",
         "jni/Utils.cpp",
         "jni/YuvToJpegEncoder.cpp",
@@ -579,6 +580,7 @@
         "utils/Color.cpp",
         "utils/LinearAllocator.cpp",
         "utils/StringUtils.cpp",
+        "utils/StatsUtils.cpp",
         "utils/TypefaceUtils.cpp",
         "utils/VectorDrawableUtils.cpp",
         "AnimationContext.cpp",
diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h
index 3a3bfb47..f6b6be0 100644
--- a/libs/hwui/ColorFilter.h
+++ b/libs/hwui/ColorFilter.h
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include "GraphicsJNI.h"
+#include "RuntimeEffectUtils.h"
 #include "SkColorFilter.h"
 
 namespace android {
@@ -113,6 +114,36 @@
     std::vector<float> mMatrix;
 };
 
+class RuntimeColorFilter : public ColorFilter {
+public:
+    RuntimeColorFilter(SkRuntimeEffectBuilder* builder) : mBuilder(builder) {}
+
+    void updateUniforms(JNIEnv* env, const char* name, const float vals[], int count,
+                        bool isColor) {
+        UpdateFloatUniforms(env, mBuilder, name, vals, count, isColor);
+        discardInstance();
+    }
+
+    void updateUniforms(JNIEnv* env, const char* name, const int vals[], int count) {
+        UpdateIntUniforms(env, mBuilder, name, vals, count);
+        discardInstance();
+    }
+
+    void updateChild(JNIEnv* env, const char* name, SkFlattenable* childEffect) {
+        UpdateChild(env, mBuilder, name, childEffect);
+        discardInstance();
+    }
+
+private:
+    sk_sp<SkColorFilter> createInstance() override {
+        // TODO: throw error if null
+        return mBuilder->makeColorFilter();
+    }
+
+private:
+    SkRuntimeEffectBuilder* mBuilder;
+};
+
 }  // namespace uirenderer
 }  // namespace android
 
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index f58dcc6..cc292d9 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -47,15 +47,16 @@
 #include <SkImage.h>
 #include <SkImageAndroid.h>
 #include <SkImagePriv.h>
+#include <SkJpegEncoder.h>
 #include <SkJpegGainmapEncoder.h>
 #include <SkPixmap.h>
+#include <SkPngEncoder.h>
 #include <SkRect.h>
 #include <SkStream.h>
-#include <SkJpegEncoder.h>
-#include <SkPngEncoder.h>
 #include <SkWebpEncoder.h>
 
 #include <atomic>
+#include <format>
 #include <limits>
 
 #ifdef __ANDROID__
diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp
index e074a27..a9a5db8 100644
--- a/libs/hwui/hwui/ImageDecoder.cpp
+++ b/libs/hwui/hwui/ImageDecoder.cpp
@@ -27,8 +27,8 @@
 #include <SkColorSpace.h>
 #include <SkColorType.h>
 #include <SkEncodedOrigin.h>
-#include <SkImageInfo.h>
 #include <SkGainmapInfo.h>
+#include <SkImageInfo.h>
 #include <SkMatrix.h>
 #include <SkPaint.h>
 #include <SkPngChunkReader.h>
@@ -43,6 +43,8 @@
 
 #include <memory>
 
+#include "modules/skcms/src/skcms_public.h"
+
 using namespace android;
 
 sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const {
diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp
index 49a7f73..8b43f1d 100644
--- a/libs/hwui/jni/BitmapFactory.cpp
+++ b/libs/hwui/jni/BitmapFactory.cpp
@@ -10,6 +10,7 @@
 #include <stdint.h>
 #include <stdio.h>
 #include <sys/stat.h>
+#include <utils/StatsUtils.h>
 
 #include <memory>
 
@@ -630,6 +631,7 @@
         }
         bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
         outputBitmap.notifyPixelsChanged();
+        uirenderer::logBitmapDecode(*reuseBitmap);
         // If a java bitmap was passed in for reuse, pass it back
         return javaBitmap;
     }
@@ -650,6 +652,7 @@
             }
         }
 
+        uirenderer::logBitmapDecode(*hardwareBitmap);
         return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
                 ninePatchChunk, ninePatchInsets, -1);
     }
@@ -659,6 +662,7 @@
         heapBitmap->setGainmap(std::move(gainmap));
     }
 
+    uirenderer::logBitmapDecode(*heapBitmap);
     // now create the java bitmap
     return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets,
                                 -1);
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index f7e8e07..5ffd5b9 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -19,6 +19,7 @@
 #include <HardwareBitmapUploader.h>
 #include <androidfw/Asset.h>
 #include <sys/stat.h>
+#include <utils/StatsUtils.h>
 
 #include <memory>
 
@@ -376,6 +377,7 @@
             recycledBitmap->setGainmap(std::move(gainmap));
         }
         bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul);
+        uirenderer::logBitmapDecode(*recycledBitmap);
         return javaBitmap;
     }
 
@@ -392,12 +394,14 @@
                 hardwareBitmap->setGainmap(std::move(gm));
             }
         }
+        uirenderer::logBitmapDecode(*hardwareBitmap);
         return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags);
     }
     Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset();
     if (hasGainmap && heapBitmap != nullptr) {
         heapBitmap->setGainmap(std::move(gainmap));
     }
+    uirenderer::logBitmapDecode(*heapBitmap);
     return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags);
 }
 
diff --git a/libs/hwui/jni/ColorFilter.cpp b/libs/hwui/jni/ColorFilter.cpp
index 0b95148..20301d2 100644
--- a/libs/hwui/jni/ColorFilter.cpp
+++ b/libs/hwui/jni/ColorFilter.cpp
@@ -18,7 +18,9 @@
 #include "ColorFilter.h"
 
 #include "GraphicsJNI.h"
+#include "RuntimeEffectUtils.h"
 #include "SkBlendMode.h"
+#include "include/effects/SkRuntimeEffect.h"
 
 namespace android {
 
@@ -89,6 +91,78 @@
             filter->setMatrix(getMatrixFromJFloatArray(env, jarray));
         }
     }
+
+    static jlong RuntimeColorFilter_createColorFilter(JNIEnv* env, jobject, jstring agsl) {
+        ScopedUtfChars strSksl(env, agsl);
+        auto result = SkRuntimeEffect::MakeForColorFilter(SkString(strSksl.c_str()),
+                                                          SkRuntimeEffect::Options{});
+        if (result.effect.get() == nullptr) {
+            doThrowIAE(env, result.errorText.c_str());
+            return 0;
+        }
+        auto builder = new SkRuntimeEffectBuilder(std::move(result.effect));
+        auto* runtimeColorFilter = new RuntimeColorFilter(builder);
+        runtimeColorFilter->incStrong(nullptr);
+        return static_cast<jlong>(reinterpret_cast<uintptr_t>(runtimeColorFilter));
+    }
+
+    static void RuntimeColorFilter_updateUniformsFloatArray(JNIEnv* env, jobject,
+                                                            jlong colorFilterPtr,
+                                                            jstring uniformName,
+                                                            jfloatArray uniforms,
+                                                            jboolean isColor) {
+        auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr);
+        ScopedUtfChars name(env, uniformName);
+        AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess);
+        if (filter) {
+            filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length(),
+                                   isColor);
+        }
+    }
+
+    static void RuntimeColorFilter_updateUniformsFloats(JNIEnv* env, jobject, jlong colorFilterPtr,
+                                                        jstring uniformName, jfloat value1,
+                                                        jfloat value2, jfloat value3, jfloat value4,
+                                                        jint count) {
+        auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr);
+        ScopedUtfChars name(env, uniformName);
+        const float values[4] = {value1, value2, value3, value4};
+        if (filter) {
+            filter->updateUniforms(env, name.c_str(), values, count, false);
+        }
+    }
+
+    static void RuntimeColorFilter_updateUniformsIntArray(JNIEnv* env, jobject,
+                                                          jlong colorFilterPtr, jstring uniformName,
+                                                          jintArray uniforms) {
+        auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr);
+        ScopedUtfChars name(env, uniformName);
+        AutoJavaIntArray autoValues(env, uniforms, 0);
+        if (filter) {
+            filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length());
+        }
+    }
+
+    static void RuntimeColorFilter_updateUniformsInts(JNIEnv* env, jobject, jlong colorFilterPtr,
+                                                      jstring uniformName, jint value1, jint value2,
+                                                      jint value3, jint value4, jint count) {
+        auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr);
+        ScopedUtfChars name(env, uniformName);
+        const int values[4] = {value1, value2, value3, value4};
+        if (filter) {
+            filter->updateUniforms(env, name.c_str(), values, count);
+        }
+    }
+
+    static void RuntimeColorFilter_updateChild(JNIEnv* env, jobject, jlong colorFilterPtr,
+                                               jstring childName, jlong childPtr) {
+        auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr);
+        ScopedUtfChars name(env, childName);
+        auto* child = reinterpret_cast<SkFlattenable*>(childPtr);
+        if (filter && child) {
+            filter->updateChild(env, name.c_str(), child);
+        }
+    }
 };
 
 static const JNINativeMethod colorfilter_methods[] = {
@@ -107,6 +181,20 @@
         {"nativeColorMatrixFilter", "([F)J", (void*)ColorFilterGlue::CreateColorMatrixFilter},
         {"nativeSetColorMatrix", "(J[F)V", (void*)ColorFilterGlue::SetColorMatrix}};
 
+static const JNINativeMethod runtime_color_filter_methods[] = {
+        {"nativeCreateRuntimeColorFilter", "(Ljava/lang/String;)J",
+         (void*)ColorFilterGlue::RuntimeColorFilter_createColorFilter},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
+         (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloatArray},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V",
+         (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloats},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V",
+         (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsIntArray},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V",
+         (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsInts},
+        {"nativeUpdateChild", "(JLjava/lang/String;J)V",
+         (void*)ColorFilterGlue::RuntimeColorFilter_updateChild}};
+
 int register_android_graphics_ColorFilter(JNIEnv* env) {
     android::RegisterMethodsOrDie(env, "android/graphics/ColorFilter", colorfilter_methods,
                                   NELEM(colorfilter_methods));
@@ -118,7 +206,10 @@
                                   NELEM(lighting_methods));
     android::RegisterMethodsOrDie(env, "android/graphics/ColorMatrixColorFilter",
                                   colormatrix_methods, NELEM(colormatrix_methods));
-    
+    android::RegisterMethodsOrDie(env, "android/graphics/RuntimeColorFilter",
+                                  runtime_color_filter_methods,
+                                  NELEM(runtime_color_filter_methods));
+
     return 0;
 }
 
diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp
index aebc4db..90fd3d8 100644
--- a/libs/hwui/jni/ImageDecoder.cpp
+++ b/libs/hwui/jni/ImageDecoder.cpp
@@ -37,6 +37,7 @@
 #include <hwui/Bitmap.h>
 #include <hwui/ImageDecoder.h>
 #include <sys/stat.h>
+#include <utils/StatsUtils.h>
 
 #include "Bitmap.h"
 #include "BitmapFactory.h"
@@ -485,6 +486,7 @@
                         hwBitmap->setGainmap(std::move(gm));
                     }
                 }
+                uirenderer::logBitmapDecode(*hwBitmap);
                 return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags,
                                             ninePatchChunk, ninePatchInsets);
             }
@@ -498,6 +500,8 @@
 
         nativeBitmap->setImmutable();
     }
+
+    uirenderer::logBitmapDecode(*nativeBitmap);
     return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk,
                                 ninePatchInsets);
 }
diff --git a/libs/hwui/jni/RuntimeEffectUtils.cpp b/libs/hwui/jni/RuntimeEffectUtils.cpp
new file mode 100644
index 0000000..46db863
--- /dev/null
+++ b/libs/hwui/jni/RuntimeEffectUtils.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+#include "RuntimeEffectUtils.h"
+
+#include "include/effects/SkRuntimeEffect.h"
+
+namespace android {
+namespace uirenderer {
+
+static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args);
+    va_end(args);
+    return ret;
+}
+
+bool isIntUniformType(const SkRuntimeEffect::Uniform::Type& type) {
+    switch (type) {
+        case SkRuntimeEffect::Uniform::Type::kFloat:
+        case SkRuntimeEffect::Uniform::Type::kFloat2:
+        case SkRuntimeEffect::Uniform::Type::kFloat3:
+        case SkRuntimeEffect::Uniform::Type::kFloat4:
+        case SkRuntimeEffect::Uniform::Type::kFloat2x2:
+        case SkRuntimeEffect::Uniform::Type::kFloat3x3:
+        case SkRuntimeEffect::Uniform::Type::kFloat4x4:
+            return false;
+        case SkRuntimeEffect::Uniform::Type::kInt:
+        case SkRuntimeEffect::Uniform::Type::kInt2:
+        case SkRuntimeEffect::Uniform::Type::kInt3:
+        case SkRuntimeEffect::Uniform::Type::kInt4:
+            return true;
+    }
+}
+
+void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName,
+                         const float values[], int count, bool isColor) {
+    SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName);
+    if (uniform.fVar == nullptr) {
+        ThrowIAEFmt(env, "unable to find uniform named %s", uniformName);
+    } else if (isColor != ((uniform.fVar->flags & SkRuntimeEffect::Uniform::kColor_Flag) != 0)) {
+        if (isColor) {
+            jniThrowExceptionFmt(
+                    env, "java/lang/IllegalArgumentException",
+                    "attempting to set a color uniform using the non-color specific APIs: %s %x",
+                    uniformName, uniform.fVar->flags);
+        } else {
+            ThrowIAEFmt(env,
+                        "attempting to set a non-color uniform using the setColorUniform APIs: %s",
+                        uniformName);
+        }
+    } else if (isIntUniformType(uniform.fVar->type)) {
+        ThrowIAEFmt(env, "attempting to set a int uniform using the setUniform APIs: %s",
+                    uniformName);
+    } else if (!uniform.set<float>(values, count)) {
+        ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]",
+                    uniform.fVar->sizeInBytes(), sizeof(float) * count);
+    }
+}
+
+void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName,
+                       const int values[], int count) {
+    SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName);
+    if (uniform.fVar == nullptr) {
+        ThrowIAEFmt(env, "unable to find uniform named %s", uniformName);
+    } else if (!isIntUniformType(uniform.fVar->type)) {
+        ThrowIAEFmt(env, "attempting to set a non-int uniform using the setIntUniform APIs: %s",
+                    uniformName);
+    } else if (!uniform.set<int>(values, count)) {
+        ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]",
+                    uniform.fVar->sizeInBytes(), sizeof(float) * count);
+    }
+}
+
+void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName,
+                 SkFlattenable* childEffect) {
+    SkRuntimeShaderBuilder::BuilderChild builderChild = builder->child(childName);
+    if (builderChild.fChild == nullptr) {
+        ThrowIAEFmt(env, "unable to find shader named %s", childName);
+        return;
+    }
+
+    builderChild = sk_ref_sp(childEffect);
+}
+
+}  // namespace uirenderer
+}  // namespace android
\ No newline at end of file
diff --git a/libs/hwui/jni/RuntimeEffectUtils.h b/libs/hwui/jni/RuntimeEffectUtils.h
new file mode 100644
index 0000000..75623c0
--- /dev/null
+++ b/libs/hwui/jni/RuntimeEffectUtils.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#ifndef RUNTIMEEFFECTUTILS_H
+#define RUNTIMEEFFECTUTILS_H
+
+#include "GraphicsJNI.h"
+#include "include/effects/SkRuntimeEffect.h"
+
+namespace android {
+namespace uirenderer {
+
+void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName,
+                         const float values[], int count, bool isColor);
+
+void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName,
+                       const int values[], int count);
+
+void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName,
+                 SkFlattenable* childEffect);
+}  // namespace uirenderer
+}  // namespace android
+
+#endif  // MAIN_RUNTIMEEFFECTUTILS_H
diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt
index 2414299..b559194 100644
--- a/libs/hwui/libhwui.map.txt
+++ b/libs/hwui/libhwui.map.txt
@@ -67,6 +67,7 @@
       SkFILEStream::SkFILEStream*;
       SkImageInfo::*;
       SkMemoryStream::SkMemoryStream*;
+      android::uirenderer::logBitmapDecode*;
     };
   local:
     *;
diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h
deleted file mode 120000
index 4fb4784..0000000
--- a/libs/hwui/platform/host/android/api-level.h
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file
diff --git a/libs/hwui/utils/StatsUtils.cpp b/libs/hwui/utils/StatsUtils.cpp
new file mode 100644
index 0000000..5c4027e
--- /dev/null
+++ b/libs/hwui/utils/StatsUtils.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright 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.
+ */
+
+#ifdef __ANDROID__
+#include <dlfcn.h>
+#include <log/log.h>
+#include <statslog_hwui.h>
+#include <statssocket_lazy.h>
+#include <utils/Errors.h>
+
+#include <mutex>
+#endif
+
+#include <unistd.h>
+
+#include "StatsUtils.h"
+
+namespace android {
+namespace uirenderer {
+
+#ifdef __ANDROID__
+
+namespace {
+
+int32_t toStatsColorSpaceTransfer(skcms_TFType transferType) {
+    switch (transferType) {
+        case skcms_TFType_sRGBish:
+            return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_SRGBISH;
+        case skcms_TFType_PQish:
+            return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_PQISH;
+        case skcms_TFType_HLGish:
+            return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_HLGISH;
+        default:
+            return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_UNKNOWN;
+    }
+}
+
+int32_t toStatsBitmapFormat(SkColorType type) {
+    switch (type) {
+        case kAlpha_8_SkColorType:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_A_8;
+        case kRGB_565_SkColorType:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGB_565;
+        case kN32_SkColorType:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_ARGB_8888;
+        case kRGBA_F16_SkColorType:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_F16;
+        case kRGBA_1010102_SkColorType:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_1010102;
+        default:
+            return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_UNKNOWN;
+    }
+}
+
+}  // namespace
+
+#endif
+
+void logBitmapDecode(const SkImageInfo& info, bool hasGainmap) {
+#ifdef __ANDROID__
+
+    if (!statssocket::lazy::IsAvailable()) {
+        std::once_flag once;
+        std::call_once(once, []() { ALOGD("libstatssocket not available, dropping stats"); });
+        return;
+    }
+
+    skcms_TFType tfnType = skcms_TFType_Invalid;
+
+    if (info.colorSpace()) {
+        skcms_TransferFunction tfn;
+        info.colorSpace()->transferFn(&tfn);
+        tfnType = skcms_TransferFunction_getType(&tfn);
+    }
+
+    auto status =
+            stats::stats_write(uirenderer::stats::IMAGE_DECODED, static_cast<int32_t>(getuid()),
+                               uirenderer::toStatsColorSpaceTransfer(tfnType), hasGainmap,
+                               uirenderer::toStatsBitmapFormat(info.colorType()));
+    ALOGW_IF(status != OK, "Image decoding logging dropped!");
+#endif
+}
+
+void logBitmapDecode(const Bitmap& bitmap) {
+    logBitmapDecode(bitmap.info(), bitmap.hasGainmap());
+}
+
+}  // namespace uirenderer
+}  // namespace android
diff --git a/libs/hwui/utils/StatsUtils.h b/libs/hwui/utils/StatsUtils.h
new file mode 100644
index 0000000..0c247014
--- /dev/null
+++ b/libs/hwui/utils/StatsUtils.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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.
+ */
+
+#pragma once
+
+#include <SkBitmap.h>
+#include <SkColorSpace.h>
+#include <SkColorType.h>
+#include <cutils/compiler.h>
+#include <hwui/Bitmap.h>
+
+namespace android {
+namespace uirenderer {
+
+ANDROID_API void logBitmapDecode(const SkImageInfo& info, bool hasGainmap);
+
+ANDROID_API void logBitmapDecode(const Bitmap& bitmap);
+
+}  // namespace uirenderer
+}  // namespace android
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 1024a55..5a8eb3a 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -16,11 +16,15 @@
 
 package android.media;
 
+import static android.media.audio.Flags.FLAG_SPEAKER_CLEANUP_USAGE;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+// TODO switch from HIDL imports to AIDL
 import android.audio.policy.configuration.V7_0.AudioUsage;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.media.audiopolicy.AudioProductStrategy;
@@ -247,6 +251,16 @@
     public static final int USAGE_ANNOUNCEMENT = SYSTEM_USAGE_OFFSET + 3;
 
     /**
+     * @hide
+     * Usage value to use when a system application plays a signal intended to clean up the
+     * speaker transducers and free them of deposits of dust or water.
+     */
+    @FlaggedApi(FLAG_SPEAKER_CLEANUP_USAGE)
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int USAGE_SPEAKER_CLEANUP = SYSTEM_USAGE_OFFSET + 4;
+
+    /**
      * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES
      *            if applicable, as well as audioattributes.proto.
      *            Also consider adding them to <aaudio/AAudio.h> for the NDK.
@@ -1522,6 +1536,8 @@
                 return "USAGE_VEHICLE_STATUS";
             case USAGE_ANNOUNCEMENT:
                 return "USAGE_ANNOUNCEMENT";
+            case USAGE_SPEAKER_CLEANUP:
+                return "USAGE_SPEAKER_CLEANUP";
             default:
                 return "unknown usage " + usage;
         }
@@ -1662,12 +1678,8 @@
     }
 
     /**
-     * @param usage one of {@link AttributeSystemUsage},
-     *     {@link AttributeSystemUsage#USAGE_CALL_ASSISTANT},
-     *     {@link AttributeSystemUsage#USAGE_EMERGENCY},
-     *     {@link AttributeSystemUsage#USAGE_SAFETY},
-     *     {@link AttributeSystemUsage#USAGE_VEHICLE_STATUS},
-     *     {@link AttributeSystemUsage#USAGE_ANNOUNCEMENT}
+     * Returns whether the given usage can only be used by system-privileged components
+     * @param usage one of {@link AttributeSystemUsage}.
      * @return boolean indicating if the usage is a system usage or not
      * @hide
      */
@@ -1677,7 +1689,8 @@
                 || usage == USAGE_EMERGENCY
                 || usage == USAGE_SAFETY
                 || usage == USAGE_VEHICLE_STATUS
-                || usage == USAGE_ANNOUNCEMENT);
+                || usage == USAGE_ANNOUNCEMENT
+                || usage == USAGE_SPEAKER_CLEANUP);
     }
 
     /**
@@ -1792,6 +1805,7 @@
             case USAGE_SAFETY:
             case USAGE_VEHICLE_STATUS:
             case USAGE_ANNOUNCEMENT:
+            case USAGE_SPEAKER_CLEANUP:
             case USAGE_UNKNOWN:
                 return AudioSystem.STREAM_MUSIC;
             default:
@@ -1831,7 +1845,8 @@
             USAGE_EMERGENCY,
             USAGE_SAFETY,
             USAGE_VEHICLE_STATUS,
-            USAGE_ANNOUNCEMENT
+            USAGE_ANNOUNCEMENT,
+            USAGE_SPEAKER_CLEANUP
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AttributeSystemUsage {}
@@ -1881,6 +1896,7 @@
         USAGE_SAFETY,
         USAGE_VEHICLE_STATUS,
         USAGE_ANNOUNCEMENT,
+        USAGE_SPEAKER_CLEANUP,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AttributeUsage {}
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index b022ea1..88efed5 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -16,12 +16,16 @@
 
 package android.media;
 
+import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN;
+
 import static com.android.media.flags.Flags.FLAG_UPDATE_CLIENT_PROFILE_PRIORITY;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.content.Context;
 import android.hardware.cas.AidlCasPluginDescriptor;
@@ -511,18 +515,20 @@
 
     private final TunerResourceManager.ResourcesReclaimListener mResourceListener =
             new TunerResourceManager.ResourcesReclaimListener() {
-            @Override
-            public void onReclaimResources() {
-                synchronized (mSessionMap) {
-                    List<Session> sessionList = new ArrayList<>(mSessionMap.keySet());
-                    for (Session casSession: sessionList) {
-                        casSession.close();
+                @Override
+                public void onReclaimResources() {
+                    synchronized (mSessionMap) {
+                        List<Session> sessionList = new ArrayList<>(mSessionMap.keySet());
+                        for (Session casSession : sessionList) {
+                            casSession.close();
+                        }
+                    }
+                    if (mEventHandler != null) {
+                        mEventHandler.sendMessage(
+                                mEventHandler.obtainMessage(EventHandler.MSG_CAS_RESOURCE_LOST));
                     }
                 }
-                mEventHandler.sendMessage(mEventHandler.obtainMessage(
-                        EventHandler.MSG_CAS_RESOURCE_LOST));
-            }
-        };
+            };
 
     /**
      * Describe a CAS plugin with its CA_system_ID and string name.
@@ -988,12 +994,32 @@
      * @param priority the new priority. Any negative value would cause no-op on priority setting
      *     and the API would only process nice value setting in that case.
      * @param niceValue the nice value.
+     * @hide
      */
     @FlaggedApi(FLAG_UPDATE_CLIENT_PROFILE_PRIORITY)
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public boolean updateResourcePriority(int priority, int niceValue) {
         return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
     }
 
+    /**
+     * Determines whether the resource holder retains ownership of the resource during a challenge
+     * scenario, when both resource holder and resource challenger have same processId and same
+     * priority.
+     *
+     * @param resourceHolderRetain Set to {@code true} to allow the resource holder to retain
+     *     ownership, or false to allow the resource challenger to acquire the resource.
+     *     If not explicitly set, resourceHolderRetain is set to {@code false}.
+     * @hide
+     */
+    @FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN)
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
+    public void setResourceHolderRetain(boolean resourceHolderRetain) {
+        mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+    }
+
     IHwBinder getBinder() {
         if (mICas != null) {
             return null; // Return IHwBinder only for HIDL
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index d49f7dd..4de6863 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -71,4 +71,12 @@
     namespace: "media_tv"
     description: "Standardize AIDL Extension Interface of TIS"
     bug: "330366987"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "set_resource_holder_retain"
+    is_exported: true
+    namespace: "media_tv"
+    description : "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA."
+    bug: "372973197"
+}
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index cdf50ec..b1adb77 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -16,6 +16,8 @@
 
 package android.media.tv.tuner;
 
+import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN;
+
 import android.annotation.BytesLong;
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
@@ -751,6 +753,21 @@
     }
 
     /**
+     * Determines whether the resource holder retains ownership of the resource during a challenge
+     * scenario, when both resource holder and resource challenger have same processId and same
+     * priority.
+     *
+     * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or
+     *     false to allow the resource challenger to acquire the resource. If not explicitly set,
+     *     resourceHolderRetain is set to false.
+     */
+    @FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN)
+    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
+    public void setResourceHolderRetain(boolean resourceHolderRetain) {
+        mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+    }
+
+    /**
      * Checks if there is an unused frontend resource available.
      *
      * @param frontendType {@link android.media.tv.tuner.frontend.FrontendSettings.Type} for the
diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
index bb581eb..be65ad9 100644
--- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
+++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
@@ -40,8 +40,11 @@
  * <p>Resources include:
  * <ul>
  * <li>TunerFrontend {@link android.media.tv.tuner.frontend}.
+ * <li>Demux {@link com.android.server.tv.tunerresourcemanager.DemuxResource}.
+ * <li>Descrambler {@link android.media.tv.tuner.Descrambler}.
  * <li>TunerLnb {@link android.media.tv.tuner.Lnb}.
  * <li>MediaCas {@link android.media.MediaCas}.
+ * <li>CiCam {@link com.android.server.tv.tunerresourcemanager.CiCamResource}.
  * <ul>
  *
  * <p>Expected workflow is:
@@ -78,7 +81,7 @@
         TUNER_RESOURCE_TYPE_LNB,
         TUNER_RESOURCE_TYPE_CAS_SESSION,
         TUNER_RESOURCE_TYPE_FRONTEND_CICAM,
-        TUNER_RESOURCE_TYPE_MAX,
+        TUNER_RESOURCE_TYPE_MAX, // upper bound of constants
      })
     @Retention(RetentionPolicy.SOURCE)
     public @interface TunerResourceType {}
@@ -220,6 +223,25 @@
     }
 
     /**
+     * Determines whether the resource holder retains ownership of the resource during a challenge
+     * scenario, when both resource holder and resource challenger have same processId and same
+     * priority.
+     *
+     * @param clientId The client id used to set ownership of resource to owner in case of resource
+     *     challenger situation.
+     * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or
+     *     false to allow the resource challenger to acquire the resource. If not explicitly set,
+     *     resourceHolderRetain is set to false.
+     */
+    public void setResourceHolderRetain(int clientId, boolean resourceHolderRetain) {
+        try {
+            mService.setResourceHolderRetain(clientId, resourceHolderRetain);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Stores the frontend resource map if it was stored before.
      *
      * <p>This API is only for testing purpose and should be used in pair with
diff --git a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
index 109c791..c57be1b0 100644
--- a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
+++ b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
@@ -151,6 +151,18 @@
      */
     void setLnbInfoList(in long[] lnbIds);
 
+    /**
+     * Determines whether the Resource Holder retains ownership of the resource during a challenge
+     * scenario, when both Resource Holder and Resource Challenger have same processId and same
+     * priority.
+     *
+     * @param clientId The resourceHolderRetain of the client is updated using client ID.
+     * @param resourceHolderRetain set to true to allow the Resource Holder to retain ownership, or
+     *     false to allow the Resource Challenger to acquire the resource. If not explicitly set,
+     *     resourceHolderRetain is set to false.
+     */
+    void setResourceHolderRetain(int clientId, boolean resourceHolderRetain);
+
     /*
      * This API is used by the Tuner framework to request a frontend from the TunerHAL.
      *
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 202535d..b025cb8 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -283,6 +283,7 @@
     ASurfaceTransaction_setEnableBackPressure; # introduced=31
     ASurfaceTransaction_setFrameRate; # introduced=30
     ASurfaceTransaction_setFrameRateWithChangeStrategy; # introduced=31
+    ASurfaceTransaction_setFrameRateParams; # introduced=36
     ASurfaceTransaction_clearFrameRate; # introduced=34
     ASurfaceTransaction_setFrameTimeline; # introduced=Tiramisu
     ASurfaceTransaction_setGeometry; # introduced=29
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index e46db6b..698bc84 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -731,6 +731,28 @@
     transaction->setFrameRate(surfaceControl, frameRate, compatibility, changeFrameRateStrategy);
 }
 
+void ASurfaceTransaction_setFrameRateParams(
+        ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl,
+        float desiredMinRate, float desiredMaxRate, float fixedSourceRate,
+        ANativeWindow_ChangeFrameRateStrategy changeFrameRateStrategy) {
+    CHECK_NOT_NULL(aSurfaceTransaction);
+    CHECK_NOT_NULL(aSurfaceControl);
+    Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction);
+    sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl);
+
+    if (desiredMaxRate < desiredMinRate) {
+        ALOGW("desiredMaxRate must be greater than or equal to desiredMinRate");
+        return;
+    }
+    // TODO(b/362798998): Fix plumbing to send modern params
+    int compatibility = fixedSourceRate == 0 ? ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT
+                                             : ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+    double frameRate = compatibility == ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
+            ? fixedSourceRate
+            : desiredMinRate;
+    transaction->setFrameRate(surfaceControl, frameRate, compatibility, changeFrameRateStrategy);
+}
+
 void ASurfaceTransaction_clearFrameRate(ASurfaceTransaction* aSurfaceTransaction,
                                         ASurfaceControl* aSurfaceControl) {
     CHECK_NOT_NULL(aSurfaceTransaction);
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 0fb3049..23dd9b7 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -48,7 +48,9 @@
         "libhwui_internal_headers",
     ],
 
-    static_libs: ["libarect"],
+    static_libs: [
+        "libarect",
+    ],
 
     host_supported: true,
     target: {
@@ -60,6 +62,11 @@
             shared_libs: [
                 "libandroid",
             ],
+            static_libs: [
+                "libstatslog_hwui",
+                "libstatspull_lazy",
+                "libstatssocket_lazy",
+            ],
             version_script: "libjnigraphics.map.txt",
         },
         host: {
diff --git a/native/graphics/jni/imagedecoder.cpp b/native/graphics/jni/imagedecoder.cpp
index e18b4a9..cb95bcf 100644
--- a/native/graphics/jni/imagedecoder.cpp
+++ b/native/graphics/jni/imagedecoder.cpp
@@ -14,18 +14,9 @@
  * limitations under the License.
  */
 
-#include "aassetstreamadaptor.h"
-
-#include <android/asset_manager.h>
-#include <android/bitmap.h>
-#include <android/data_space.h>
-#include <android/imagedecoder.h>
 #include <MimeType.h>
-#include <android/rect.h>
-#include <hwui/ImageDecoder.h>
-#include <log/log.h>
-#include <SkAndroidCodec.h>
 #include <SkAlphaType.h>
+#include <SkAndroidCodec.h>
 #include <SkCodec.h>
 #include <SkCodecAnimation.h>
 #include <SkColorSpace.h>
@@ -35,14 +26,24 @@
 #include <SkRefCnt.h>
 #include <SkSize.h>
 #include <SkStream.h>
-#include <utils/Color.h>
-
+#include <android/asset_manager.h>
+#include <android/bitmap.h>
+#include <android/data_space.h>
+#include <android/imagedecoder.h>
+#include <android/rect.h>
 #include <fcntl.h>
-#include <limits>
-#include <optional>
+#include <hwui/ImageDecoder.h>
+#include <log/log.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
+#include <utils/Color.h>
+#include <utils/StatsUtils.h>
+
+#include <limits>
+#include <optional>
+
+#include "aassetstreamadaptor.h"
 
 using namespace android;
 
@@ -400,9 +401,7 @@
     return info.minRowBytes();
 }
 
-int AImageDecoder_decodeImage(AImageDecoder* decoder,
-                              void* pixels, size_t stride,
-                              size_t size) {
+int AImageDecoder_decodeImage(AImageDecoder* decoder, void* pixels, size_t stride, size_t size) {
     if (!decoder || !pixels || !stride) {
         return ANDROID_IMAGE_DECODER_BAD_PARAMETER;
     }
@@ -419,7 +418,13 @@
         return ANDROID_IMAGE_DECODER_FINISHED;
     }
 
-    return ResultToErrorCode(imageDecoder->decode(pixels, stride));
+    const auto result = ResultToErrorCode(imageDecoder->decode(pixels, stride));
+
+    if (result == ANDROID_IMAGE_DECODER_SUCCESS) {
+        uirenderer::logBitmapDecode(imageDecoder->getOutputInfo(), false);
+    }
+
+    return result;
 }
 
 void AImageDecoder_delete(AImageDecoder* decoder) {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt
index be08606..99d3c8d 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt
@@ -24,7 +24,7 @@
  * Adapter of [Handler] and [Executor], where the task is executed on handler with given looper.
  *
  * When current looper is same with the given looper, task passed to [Executor.execute] will be
- * executed immediately to improve better performance.
+ * executed immediately to achieve better performance.
  *
  * @param looper Looper of the handler.
  */
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
index 9cb0ebb..843d2aa 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
@@ -114,6 +114,10 @@
     fun notifyChange(key: K, reason: Int)
 }
 
+/** Delegation of [KeyedObservable]. */
+open class KeyedObservableDelegate<K>(delegate: KeyedObservable<K>) :
+    KeyedObservable<K> by delegate
+
 /** A thread safe implementation of [KeyedObservable]. */
 open class KeyedDataObservable<K> : KeyedObservable<K> {
     // Instead of @GuardedBy("this"), guarded by itself because KeyedDataObservable object could be
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
index e93d756..cbe602e 100644
--- a/packages/SettingsLib/Graph/graph.proto
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -74,6 +74,9 @@
   optional ActionTarget action_target = 12;
   // Preference value (if present, it means `persistent` is true).
   optional PreferenceValueProto value = 13;
+  // Intent to show and locate the preference (might have highlight animation on
+  // the preference).
+  optional IntentProto launch_intent = 14;
 
   // Target of an Intent
   message ActionTarget {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index 5e78a29..9cb872a 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -51,9 +51,9 @@
 import com.android.settingslib.metadata.PreferenceTitleProvider
 import com.android.settingslib.preference.PreferenceScreenFactory
 import com.android.settingslib.preference.PreferenceScreenProvider
+import java.util.Locale
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
-import java.util.Locale
 
 private const val TAG = "PreferenceGraphBuilder"
 
@@ -140,7 +140,7 @@
         addPreferenceScreen(metadata.key) {
             preferenceScreenProto {
                 completeHierarchy = metadata.hasCompleteHierarchy()
-                root = metadata.getPreferenceHierarchy(context).toProto(true)
+                root = metadata.getPreferenceHierarchy(context).toProto(metadata, true)
             }
         }
 
@@ -237,23 +237,29 @@
         this@toProto.intent?.let { actionTarget = it.toActionTarget() }
     }
 
-    private suspend fun PreferenceHierarchy.toProto(isRoot: Boolean): PreferenceGroupProto =
-        preferenceGroupProto {
-            preference = toProto(this@toProto, isRoot)
-            forEachAsync {
-                addPreferences(
-                    preferenceOrGroupProto {
-                        if (it is PreferenceHierarchy) {
-                            group = it.toProto(false)
-                        } else {
-                            preference = toProto(it, false)
-                        }
+    private suspend fun PreferenceHierarchy.toProto(
+        screenMetadata: PreferenceScreenMetadata,
+        isRoot: Boolean,
+    ): PreferenceGroupProto = preferenceGroupProto {
+        preference = toProto(screenMetadata, this@toProto, isRoot)
+        forEachAsync {
+            addPreferences(
+                preferenceOrGroupProto {
+                    if (it is PreferenceHierarchy) {
+                        group = it.toProto(screenMetadata, false)
+                    } else {
+                        preference = toProto(screenMetadata, it, false)
                     }
-                )
-            }
+                }
+            )
         }
+    }
 
-    private suspend fun toProto(node: PreferenceHierarchyNode, isRoot: Boolean) = preferenceProto {
+    private suspend fun toProto(
+        screenMetadata: PreferenceScreenMetadata,
+        node: PreferenceHierarchyNode,
+        isRoot: Boolean,
+    ) = preferenceProto {
         val metadata = node.metadata
         key = metadata.key
         metadata.getTitleTextProto(isRoot)?.let { title = it }
@@ -291,6 +297,7 @@
             @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata)
         }
         metadata.intent(context)?.let { actionTarget = it.toActionTarget() }
+        screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() }
     }
 
     private fun PreferenceMetadata.getTitleTextProto(isRoot: Boolean): TextProto? {
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
index 10087a7..386b6d9 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -162,8 +162,8 @@
     /**
      * Returns the preference icon.
      *
-     * Implement [PreferenceIconProvider] interface if icon content is provided dynamically
-     * (e.g. icon is provided based on flag value).
+     * Implement [PreferenceIconProvider] interface if icon is provided dynamically (e.g. icon is
+     * provided based on flag value).
      */
     fun getPreferenceIcon(context: Context): Int =
         when {
@@ -212,4 +212,11 @@
      * conditions. DO NOT check any condition (except compile time flag) before adding a preference.
      */
     fun getPreferenceHierarchy(context: Context): PreferenceHierarchy
+
+    /**
+     * Returns the [Intent] to show current preference screen.
+     *
+     * @param metadata the preference to locate when show the screen
+     */
+    fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null
 }
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 7af9c81..d75f3e8 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -237,7 +237,8 @@
                 } else {
                     preferences[preference.key]?.let {
                         preferenceBindingFactory.bind(preference, it)
-                        (it as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+                        val metadata = it.metadata
+                        (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
                             preference.preferenceDataStore =
                                 storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
                         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 744e97e..1998d0c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -1,7 +1,6 @@
 package com.android.settingslib;
 
 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
-import static android.webkit.Flags.updateServiceV2;
 
 import android.annotation.ColorInt;
 import android.app.admin.DevicePolicyManager;
@@ -496,7 +495,7 @@
                 || packageName.equals(sServicesSystemSharedLibPackageName)
                 || packageName.equals(sSharedSystemSharedLibPackageName)
                 || packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
-                || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName(pm)))
+                || packageName.equals(getDefaultWebViewPackageName(pm))
                 || isDeviceProvisioningPackage(resources, packageName);
     }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 509b88b..558ccaf 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -263,5 +263,6 @@
         VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ALL, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, ANY_STRING_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 1659c9e..c2beaa8 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -944,7 +944,8 @@
                         Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE,
                         Settings.System.WEAR_TTS_PREWARM_ENABLED,
                         Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
-                        Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific
+                        Settings.System.MULTI_AUDIO_FOCUS_ENABLED, // form-factor/OEM specific
+                        Settings.System.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME // internal cache
                 );
         if (!Flags.backUpSmoothDisplayAndForcePeakRefreshRate()) {
             settings.add(Settings.System.MIN_REFRESH_RATE);
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4531b79..0537b14 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -30,6 +30,7 @@
     <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" />
 
     <!-- Used to read storage for all users -->
+    <uses-permission android:name="android.permission.STORAGE_INTERNAL" />
     <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -1137,5 +1138,11 @@
                 android:name="android.service.dream"
                 android:resource="@xml/home_controls_dream_metadata" />
         </service>
+
+        <service android:name="com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService"
+            android:singleUser="true"
+            android:exported="false"
+            />
+
     </application>
 </manifest>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7921ce0..83b7566 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -59,7 +59,7 @@
 flag {
    name: "notification_over_expansion_clipping_fix"
    namespace: "systemui"
-   description: "fix NSSL clipping when over-expanding; fixes split shade bug."
+   description: "Fix NSSL clipping when over-expanding; fixes split shade bug."
    bug: "288553572"
    metadata {
         purpose: PURPOSE_BUGFIX
@@ -67,6 +67,14 @@
 }
 
 flag {
+    name: "notification_add_x_on_hover_to_dismiss"
+    namespace: "systemui"
+    description: "Adds an x to notifications which shows up on mouse hover, allowing the user to "
+        "dismiss a notification with mouse."
+    bug: "376297472"
+}
+
+flag {
     name: "notification_async_group_header_inflation"
     namespace: "systemui"
     description: "Inflates the notification group summary header views from the background thread."
@@ -740,10 +748,13 @@
 }
 
 flag {
-    name: "smartspace_swipe_event_logging"
+    name: "smartspace_swipe_event_logging_fix"
     namespace: "systemui"
     description: "Log card swipe events in smartspace"
     bug: "374150422"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+   }
 }
 
 flag {
@@ -1143,6 +1154,13 @@
 }
 
 flag {
+  name: "communal_hub_on_mobile"
+  namespace: "systemui"
+  description: "Brings the glanceable hub experience to mobile phones"
+  bug: "375689917"
+}
+
+flag {
     name: "dream_overlay_updated_font"
     namespace: "systemui"
     description: "Flag to enable updated font settings for dream overlay"
@@ -1615,6 +1633,16 @@
 }
 
 flag {
+  name: "home_controls_dream_hsum"
+  namespace: "systemui"
+  description: "Enables the home controls dream in HSUM"
+  bug: "370691405"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
    name: "only_show_media_stream_slider_in_single_volume_mode"
    namespace: "systemui"
    description: "When the device is in single volume mode, only show media stream slider and hide all other stream (e.g. call, notification, alarm, etc) sliders in volume panel"
@@ -1642,6 +1670,13 @@
 }
 
 flag {
+    name: "bouncer_ui_revamp"
+    namespace: "systemui"
+    description: "Updates to background (blur), button animations and font changes."
+    bug: "376491880"
+}
+
+flag {
   name: "ensure_enr_views_visibility"
   namespace: "systemui"
   description: "Ensures public and private visibilities"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
index 6e83124..82d1436 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
@@ -32,11 +32,7 @@
  * Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors.
  */
 @Composable
-fun Icon(
-    icon: Icon,
-    modifier: Modifier = Modifier,
-    tint: Color = LocalContentColor.current,
-) {
+fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentColor.current) {
     val contentDescription = icon.contentDescription?.load()
     when (icon) {
         is Icon.Loaded -> {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 6f1349f..9d4408a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -45,11 +45,13 @@
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexContentPicker
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.res.R
 
 /** Renders a lightweight shade UI container, as an overlay. */
 @Composable
@@ -104,13 +106,11 @@
 @Composable
 private fun Modifier.panelSize(): Modifier {
     val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
-
     return this.then(
-        when (widthSizeClass) {
-            WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth()
-            WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium)
-            WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge)
-            else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"")
+        if (widthSizeClass == WindowWidthSizeClass.Compact) {
+            Modifier.fillMaxWidth()
+        } else {
+            Modifier.width(dimensionResource(id = R.dimen.shade_panel_width))
         }
     )
 }
@@ -176,8 +176,6 @@
     object Dimensions {
         val ScrimContentPadding = 16.dp
         val PanelCornerRadius = 46.dp
-        val PanelWidthMedium = 390.dp
-        val PanelWidthLarge = 474.dp
         val OverscrollLimit = 32.dp
     }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index f14622f..63c5d7a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -48,7 +48,6 @@
 import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.fastForEachReversed
 import androidx.compose.ui.util.lerp
-import com.android.compose.animation.scene.Element.State
 import com.android.compose.animation.scene.content.Content
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.transformation.PropertyTransformation
@@ -163,7 +162,7 @@
     transitionStates: List<TransitionState>,
 ): Modifier {
     fun isSharedElement(
-        stateByContent: Map<ContentKey, State>,
+        stateByContent: Map<ContentKey, Element.State>,
         transition: TransitionState.Transition,
     ): Boolean {
         fun inFromContent() = transition.fromContent in stateByContent
@@ -1281,14 +1280,14 @@
                 checkNotNull(if (currentContent == toContent) toState else fromState)
             val idleValue = contentValue(overscrollState)
             val targetValue =
-                propertySpec.transform(
-                    layoutImpl,
-                    currentContent,
-                    element,
-                    overscrollState,
-                    transition,
-                    idleValue,
-                )
+                with(propertySpec) {
+                    layoutImpl.propertyTransformationScope.transform(
+                        currentContent,
+                        element.key,
+                        transition,
+                        idleValue,
+                    )
+                }
 
             // Make sure we don't read progress if values are the same and we don't need to
             // interpolate, so we don't invalidate the phase where this is read.
@@ -1376,24 +1375,26 @@
         val idleValue = contentValue(contentState)
         val isEntering = content == toContent
         val previewTargetValue =
-            previewTransformation.transform(
-                layoutImpl,
-                content,
-                element,
-                contentState,
-                transition,
-                idleValue,
-            )
+            with(previewTransformation) {
+                layoutImpl.propertyTransformationScope.transform(
+                    content,
+                    element.key,
+                    transition,
+                    idleValue,
+                )
+            }
 
         val targetValueOrNull =
-            transformation?.transform(
-                layoutImpl,
-                content,
-                element,
-                contentState,
-                transition,
-                idleValue,
-            )
+            transformation?.let { transformation ->
+                with(transformation) {
+                    layoutImpl.propertyTransformationScope.transform(
+                        content,
+                        element.key,
+                        transition,
+                        idleValue,
+                    )
+                }
+            }
 
         // Make sure we don't read progress if values are the same and we don't need to interpolate,
         // so we don't invalidate the phase where this is read.
@@ -1460,7 +1461,14 @@
 
     val idleValue = contentValue(contentState)
     val targetValue =
-        transformation.transform(layoutImpl, content, element, contentState, transition, idleValue)
+        with(transformation) {
+            layoutImpl.propertyTransformationScope.transform(
+                content,
+                element.key,
+                transition,
+                idleValue,
+            )
+        }
 
     // Make sure we don't read progress if values are the same and we don't need to interpolate, so
     // we don't invalidate the phase where this is read.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index d58d3f24..e93cf8f7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -129,6 +129,7 @@
     private val verticalDraggableHandler: DraggableHandlerImpl
 
     internal val elementStateScope = ElementStateScopeImpl(this)
+    internal val propertyTransformationScope = PropertyTransformationScopeImpl(this)
     private var _userActionDistanceScope: UserActionDistanceScope? = null
     internal val userActionDistanceScope: UserActionDistanceScope
         get() =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 690c809..8457481 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.transformation.PropertyTransformationScope
 
 internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayoutImpl) :
     ElementStateScope {
@@ -46,3 +48,15 @@
     override val fontScale: Float
         get() = layoutImpl.density.fontScale
 }
+
+internal class PropertyTransformationScopeImpl(private val layoutImpl: SceneTransitionLayoutImpl) :
+    PropertyTransformationScope, ElementStateScope by layoutImpl.elementStateScope {
+    override val density: Float
+        get() = layoutImpl.density.density
+
+    override val fontScale: Float
+        get() = layoutImpl.density.fontScale
+
+    override val layoutDirection: LayoutDirection
+        get() = layoutImpl.layoutDirection
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index c5a3067c..5936d25 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -18,10 +18,8 @@
 
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Anchor the size of an element to the size of another element. */
@@ -31,19 +29,15 @@
     private val anchorWidth: Boolean,
     private val anchorHeight: Boolean,
 ) : PropertyTransformation<IntSize> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
         fun anchorSizeIn(content: ContentKey): IntSize {
             val size =
-                layoutImpl.elements[anchor]?.stateByContent?.get(content)?.targetSize?.takeIf {
-                    it != Element.SizeUnspecified
-                }
+                anchor.targetSize(content)
                     ?: throwMissingAnchorException(
                         transformation = "AnchoredSize",
                         anchor = anchor,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 86e06ab..0a59dfe 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -17,12 +17,9 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.isSpecified
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Anchor the translation of an element to another element. */
@@ -30,11 +27,9 @@
     override val matcher: ElementMatcher,
     private val anchor: ElementKey,
 ) : PropertyTransformation<Offset> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -46,18 +41,13 @@
             )
         }
 
-        val anchor = layoutImpl.elements[anchor] ?: throwException(content = null)
-        fun anchorOffsetIn(content: ContentKey): Offset? {
-            return anchor.stateByContent[content]?.targetOffset?.takeIf { it.isSpecified }
-        }
-
         // [element] will move the same amount as [anchor] does.
         // TODO(b/290184746): Also support anchors that are not shared but translated because of
         // other transformations, like an edge translation.
         val anchorFromOffset =
-            anchorOffsetIn(transition.fromContent) ?: throwException(transition.fromContent)
+            anchor.targetOffset(transition.fromContent) ?: throwException(transition.fromContent)
         val anchorToOffset =
-            anchorOffsetIn(transition.toContent) ?: throwException(transition.toContent)
+            anchor.targetOffset(transition.toContent) ?: throwException(transition.toContent)
         val offset = anchorToOffset - anchorFromOffset
 
         return if (content == transition.toContent) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 7f86479..7223dad 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -18,10 +18,9 @@
 
 import androidx.compose.ui.geometry.Offset
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /**
@@ -35,11 +34,9 @@
     private val pivot: Offset = Offset.Unspecified,
 ) : PropertyTransformation<Scale> {
 
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Scale,
     ): Scale {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 031f50e..4ae07c5 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -19,9 +19,8 @@
 import androidx.compose.ui.geometry.Offset
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Translate an element from an edge of the layout. */
@@ -30,21 +29,18 @@
     private val edge: Edge,
     private val startsOutsideLayoutBounds: Boolean = true,
 ) : PropertyTransformation<Offset> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
-        val sceneSize = layoutImpl.content(content).targetSize
-        val elementSize = stateInContent.targetSize
-        if (elementSize == Element.SizeUnspecified) {
-            return value
-        }
+        val sceneSize =
+            content.targetSize()
+                ?: error("Content ${content.debugName} does not have a target size")
+        val elementSize = element.targetSize(content) ?: return value
 
-        return when (edge.resolve(layoutImpl.layoutDirection)) {
+        return when (edge.resolve(layoutDirection)) {
             Edge.Resolved.Top ->
                 if (startsOutsideLayoutBounds) {
                     Offset(value.x, -elementSize.height.toFloat())
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index 078aa0f..c11ec97 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -17,18 +17,15 @@
 package com.android.compose.animation.scene.transformation
 
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** Fade an element in or out. */
 internal class Fade(override val matcher: ElementMatcher) : PropertyTransformation<Float> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Float,
     ): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index 5f3fdaf..a159a5b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -18,9 +18,8 @@
 
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 import kotlin.math.roundToInt
 
@@ -33,11 +32,9 @@
     private val width: Float = 1f,
     private val height: Float = 1f,
 ) : PropertyTransformation<IntSize> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index de7f418..d38067d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -18,13 +18,15 @@
 
 import androidx.compose.animation.core.Easing
 import androidx.compose.animation.core.LinearEasing
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastCoerceAtLeast
 import androidx.compose.ui.util.fastCoerceAtMost
 import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.ElementStateScope
 import com.android.compose.animation.scene.content.state.TransitionState
 
 /** A transformation applied to one or more elements during a transition. */
@@ -56,22 +58,30 @@
 ) : Transformation
 
 /** A transformation that changes the value of an element property, like its size or offset. */
-internal sealed interface PropertyTransformation<T> : Transformation {
+interface PropertyTransformation<T> : Transformation {
     /**
-     * Transform [value], i.e. the value of the transformed property without this transformation.
+     * Return the transformed value for the given property, i.e.:
+     * - the value at progress = 0% for elements that are entering the layout (i.e. elements in the
+     *   content we are transitioning to).
+     * - the value at progress = 100% for elements that are leaving the layout (i.e. elements in the
+     *   content we are transitioning from).
+     *
+     * The returned value will be interpolated using the [transition] progress and [value], the
+     * value of the property when we are idle.
      */
-    // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
-    // to these internal classes.
-    fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: T,
     ): T
 }
 
+interface PropertyTransformationScope : Density, ElementStateScope {
+    /** The current [direction][LayoutDirection] of the layout. */
+    val layoutDirection: LayoutDirection
+}
+
 /**
  * A [PropertyTransformation] associated to a range. This is a helper class so that normal
  * implementations of [PropertyTransformation] don't have to take care of reversing their range when
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 7014271..af0a6ed 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -21,10 +21,9 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.content.state.TransitionState
 
 internal class Translate(
@@ -32,15 +31,13 @@
     private val x: Dp = 0.dp,
     private val y: Dp = 0.dp,
 ) : PropertyTransformation<Offset> {
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
-        return with(layoutImpl.density) { Offset(value.x + x.toPx(), value.y + y.toPx()) }
+        return Offset(value.x + x.toPx(), value.y + y.toPx())
     }
 }
 
@@ -51,11 +48,9 @@
 ) : PropertyTransformation<Offset> {
     private val cachedOverscrollScope = CachedOverscrollScope()
 
-    override fun transform(
-        layoutImpl: SceneTransitionLayoutImpl,
+    override fun PropertyTransformationScope.transform(
         content: ContentKey,
-        element: Element,
-        stateInContent: Element.State,
+        element: ElementKey,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -64,7 +59,7 @@
         // that this method was invoked after performing this check.
         val overscrollProperties = transition as TransitionState.HasOverscrollProperties
         val overscrollScope =
-            cachedOverscrollScope.getFromCacheOrCompute(layoutImpl.density, overscrollProperties)
+            cachedOverscrollScope.getFromCacheOrCompute(density = this, overscrollProperties)
 
         return Offset(x = value.x + overscrollScope.x(), y = value.y + overscrollScope.y())
     }
@@ -75,7 +70,7 @@
  * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame
  * whenever an overscroll transition is computed.
  */
-private class CachedOverscrollScope() {
+private class CachedOverscrollScope {
     private var previousScope: OverscrollScope? = null
     private var previousDensity: Density? = null
     private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index a883977..4ed8fd8 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -27,7 +27,7 @@
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceEvents
-import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
@@ -103,7 +103,9 @@
                 view.onZenDataChanged(data)
             }
 
-            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+            override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
+                view.updateAxes(axes)
+            }
 
             override var isReactiveTouchInteractionEnabled
                 get() = view.isReactiveTouchInteractionEnabled
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index c5b7518..7014826 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -33,8 +33,8 @@
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.ClockMessageBuffers
-import com.android.systemui.plugins.clocks.ClockReactiveSetting
 import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
 import com.android.systemui.plugins.clocks.ThemeConfig
@@ -264,7 +264,7 @@
 
         override fun onZenDataChanged(data: ZenData) {}
 
-        override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+        override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {}
     }
 
     open inner class DefaultClockAnimations(
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index a89e6fb..d70d61c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -21,12 +21,12 @@
 import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.plugins.clocks.AxisType
 import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.ClockFontAxis
 import com.android.systemui.plugins.clocks.ClockId
 import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockMetadata
 import com.android.systemui.plugins.clocks.ClockPickerConfig
 import com.android.systemui.plugins.clocks.ClockProvider
-import com.android.systemui.plugins.clocks.ClockReactiveAxis
 import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.shared.clocks.view.HorizontalAlignment
 import com.android.systemui.shared.clocks.view.VerticalAlignment
@@ -91,15 +91,42 @@
             axes =
                 if (isClockReactiveVariantsEnabled)
                     listOf(
-                        ClockReactiveAxis(
-                            key = "wdth",
-                            type = AxisType.Slider,
-                            maxValue = 1000f,
-                            minValue = 100f,
+                        ClockFontAxis(
+                            key = "wght",
+                            type = AxisType.Float,
+                            minValue = 1f,
                             currentValue = 400f,
+                            maxValue = 1000f,
+                            name = "Weight",
+                            description = "Glyph Weight",
+                        ),
+                        ClockFontAxis(
+                            key = "wdth",
+                            type = AxisType.Float,
+                            minValue = 25f,
+                            currentValue = 100f,
+                            maxValue = 151f,
                             name = "Width",
                             description = "Glyph Width",
-                        )
+                        ),
+                        ClockFontAxis(
+                            key = "ROND",
+                            type = AxisType.Boolean,
+                            minValue = 0f,
+                            currentValue = 0f,
+                            maxValue = 100f,
+                            name = "Round",
+                            description = "Glyph Roundness",
+                        ),
+                        ClockFontAxis(
+                            key = "slnt",
+                            type = AxisType.Boolean,
+                            minValue = 0f,
+                            currentValue = 0f,
+                            maxValue = -10f,
+                            name = "Slant",
+                            description = "Glyph Slant",
+                        ),
                     )
                 else listOf(),
         )
@@ -121,12 +148,12 @@
                                         FontTextStyle(
                                             lineHeight = 147.25f,
                                             fontVariation =
-                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                         ),
                                     aodStyle =
                                         FontTextStyle(
                                             fontVariation =
-                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -147,12 +174,12 @@
                                         FontTextStyle(
                                             lineHeight = 147.25f,
                                             fontVariation =
-                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                         ),
                                     aodStyle =
                                         FontTextStyle(
                                             fontVariation =
-                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -173,12 +200,12 @@
                                         FontTextStyle(
                                             lineHeight = 147.25f,
                                             fontVariation =
-                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                         ),
                                     aodStyle =
                                         FontTextStyle(
                                             fontVariation =
-                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -199,12 +226,12 @@
                                         FontTextStyle(
                                             lineHeight = 147.25f,
                                             fontVariation =
-                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                         ),
                                     aodStyle =
                                         FontTextStyle(
                                             fontVariation =
-                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -229,12 +256,14 @@
                         timespec = DigitalTimespec.TIME_FULL_FORMAT,
                         style =
                             FontTextStyle(
-                                fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                fontVariation =
+                                    "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                 fontSizeScale = 0.98f,
                             ),
                         aodStyle =
                             FontTextStyle(
-                                fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                fontVariation =
+                                    "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100, 'slnt' 0",
                                 fillColorLight = "#FFFFFFFF",
                                 outlineColor = "#00000000",
                                 renderType = RenderType.CHANGE_WEIGHT,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index a75022a..d8fd776 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -23,8 +23,8 @@
 import com.android.systemui.plugins.clocks.ClockConfig
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.ClockMessageBuffers
-import com.android.systemui.plugins.clocks.ClockReactiveSetting
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
@@ -113,9 +113,9 @@
                 largeClock.events.onZenDataChanged(data)
             }
 
-            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {
-                smallClock.events.onReactiveAxesChanged(axes)
-                largeClock.events.onReactiveAxesChanged(axes)
+            override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
+                smallClock.events.onFontAxesChanged(axes)
+                largeClock.events.onFontAxesChanged(axes)
             }
         }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index 8ffc00d..a4782ac 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockFaceEvents
 import com.android.systemui.plugins.clocks.ClockFaceLayout
-import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
@@ -136,13 +136,16 @@
 
         override fun onFontSettingChanged(fontSizePx: Float) {
             layerController.faceEvents.onFontSettingChanged(fontSizePx)
+            view.requestLayout()
         }
 
         override fun onThemeChanged(theme: ThemeConfig) {
             layerController.faceEvents.onThemeChanged(theme)
         }
 
-        override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+        override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
+            layerController.events.onFontAxesChanged(axes)
+        }
 
         /**
          * targetRegion passed to all customized clock applies counter translationY of
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index 7b1960e..143b28f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceEvents
-import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
@@ -246,7 +246,9 @@
 
             override fun onZenDataChanged(data: ZenData) {}
 
-            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+            override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
+                view.updateAxes(axes)
+            }
         }
 
     override val animations =
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
index ce4d71c..b09332f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.shared.clocks.LogUtil
@@ -126,6 +127,11 @@
         invalidate()
     }
 
+    fun updateAxes(axes: List<ClockFontAxisSetting>) {
+        digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
+        requestLayout()
+    }
+
     fun onFontSettingChanged(fontSizePx: Float) {
         digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
     }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 25b2ad7..edcee9d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -39,8 +39,7 @@
     DigitalClockFaceView(context, messageBuffer) {
     override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
     val digitLeftTopMap = mutableMapOf<Int, Point>()
-    var maxSingleDigitHeight = -1
-    var maxSingleDigitWidth = -1
+    var maxSingleDigitSize = Point(-1, -1)
     val lockscreenTranslate = Point(0, 0)
     val aodTranslate = Point(0, 0)
 
@@ -119,25 +118,26 @@
     }
 
     protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+        maxSingleDigitSize = Point(-1, -1)
         digitalClockTextViewMap.forEach { (_, textView) ->
             textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+            maxSingleDigitSize.x = max(maxSingleDigitSize.x, textView.measuredWidth)
+            maxSingleDigitSize.y = max(maxSingleDigitSize.y, textView.measuredHeight)
         }
         val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!!
-        maxSingleDigitHeight = textView.measuredHeight
-        maxSingleDigitWidth = textView.measuredWidth
-        aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt()
-        aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt()
+        aodTranslate.x = -(maxSingleDigitSize.x * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt()
+        aodTranslate.y = (maxSingleDigitSize.y * AOD_VERTICAL_TRANSLATE_RATIO).toInt()
         return Point(
-            ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2),
-            ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2),
+            ((maxSingleDigitSize.x + abs(aodTranslate.x)) * 2),
+            ((maxSingleDigitSize.y + abs(aodTranslate.y)) * 2),
         )
     }
 
     protected override fun calculateLeftTopPosition() {
         digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
-        digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0)
-        digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight)
-        digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight)
+        digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
+        digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
+        digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize)
         digitLeftTopMap.forEach { _, point ->
             point.x += abs(aodTranslate.x)
             point.y += abs(aodTranslate.y)
@@ -162,7 +162,7 @@
     override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
         dozeControlState.animateDoze = {
             super.animateDoze(isDozing, isAnimated)
-            if (maxSingleDigitHeight == -1) {
+            if (maxSingleDigitSize.x < 0 || maxSingleDigitSize.y < 0) {
                 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
             }
             digitalClockTextViewMap.forEach { (id, textView) ->
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index baed3ae..0dacce1 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.ClockAnimation
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
@@ -140,6 +141,25 @@
         invalidate()
     }
 
+    override fun updateAxes(axes: List<ClockFontAxisSetting>) {
+        val sb = StringBuilder()
+        sb.append("'opsz' 144")
+
+        for (axis in axes) {
+            if (sb.length > 0) sb.append(", ")
+            sb.append("'${axis.key}' ${axis.value.toInt()}")
+        }
+
+        val fvar = sb.toString()
+        lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
+        typeface = lockScreenPaint.typeface
+        textAnimator.setTextStyle(fvar = fvar, animate = true)
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+        recomputeMaxSingleDigitSizes()
+        requestLayout()
+        invalidate()
+    }
+
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.d("onMeasure()")
         if (isVertical) {
@@ -189,6 +209,7 @@
                     MeasureSpec.getMode(measuredHeight),
                 )
         }
+
         if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) {
             expectedWidth = widthMeasureSpec
         } else {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
index 30b54d8..3d2ed4a1 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shared.clocks.view
 
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.TextStyle
 
@@ -34,6 +35,8 @@
 
     fun updateColor(color: Int)
 
+    fun updateAxes(axes: List<ClockFontAxisSetting>)
+
     fun refreshTime()
 
     fun animateCharge()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
index 4856f15..e142169 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -3,7 +3,9 @@
 import android.app.admin.DevicePolicyManager
 import android.app.admin.DevicePolicyResourcesManager
 import android.content.pm.UserInfo
+import android.hardware.biometrics.Flags
 import android.os.UserManager
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
@@ -33,6 +35,7 @@
 import org.mockito.junit.MockitoJUnit
 
 private const val USER_ID = 22
+private const val OWNER_ID = 10
 private const val OPERATION_ID = 100L
 private const val MAX_ATTEMPTS = 5
 
@@ -67,7 +70,7 @@
                 lockPatternUtils,
                 userManager,
                 devicePolicyManager,
-                systemClock
+                systemClock,
             )
     }
 
@@ -115,58 +118,87 @@
 
     @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000))
 
-    private fun pinCredential(result: VerifyCredentialResponse) = runTest {
-        val usedAttempts = 1
-        whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
-            .thenReturn(usedAttempts)
-        whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result)
-        whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID)))
-            .thenReturn(result)
-        whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer {
-            systemClock.elapsedRealtime() + (it.arguments[1] as Int)
-        }
+    @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP)
+    @Test
+    fun pinCredentialTiedProfileWhenGood() = pinCredential(goodCredential(), OWNER_ID)
 
-        // wrap in an async block so the test can advance the clock if throttling credential
-        // checks prevents the method from returning
-        val statusList = mutableListOf<CredentialStatus>()
-        interactor
-            .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
-            .toList(statusList)
+    @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP)
+    @Test
+    fun pinCredentialTiedProfileWhenBad() = pinCredential(badCredential(), OWNER_ID)
 
-        val last = statusList.removeLastOrNull()
-        if (result.isMatched) {
-            assertThat(statusList).isEmpty()
-            val successfulResult = last as? CredentialStatus.Success.Verified
-            assertThat(successfulResult).isNotNull()
-            assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT)
+    @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP)
+    @Test
+    fun pinCredentialTiedProfileWhenBadAndThrottled() =
+        pinCredential(badCredential(timeout = 5_000), OWNER_ID)
 
-            verify(lockPatternUtils).userPresent(eq(USER_ID))
-            verify(lockPatternUtils)
-                .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle))
-        } else {
-            val failedResult = last as? CredentialStatus.Fail.Error
-            assertThat(failedResult).isNotNull()
-            assertThat(failedResult!!.remainingAttempts)
-                .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1)
-            assertThat(failedResult.urgentMessage).isNull()
+    private fun pinCredential(result: VerifyCredentialResponse, credentialOwner: Int = USER_ID) =
+        runTest {
+            val usedAttempts = 1
+            whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+                .thenReturn(usedAttempts)
+            whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+                .thenReturn(result)
+            whenever(lockPatternUtils.verifyTiedProfileChallenge(any(), eq(USER_ID), anyInt()))
+                .thenReturn(result)
+            whenever(
+                    lockPatternUtils.verifyGatekeeperPasswordHandle(
+                        anyLong(),
+                        anyLong(),
+                        eq(USER_ID),
+                    )
+                )
+                .thenReturn(result)
+            whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer {
+                systemClock.elapsedRealtime() + (it.arguments[1] as Int)
+            }
 
-            if (result.timeout > 0) { // failed and throttled
-                // messages are in the throttled errors, so the final Error.error is empty
-                assertThat(failedResult.error).isEmpty()
-                assertThat(statusList).isNotEmpty()
-                assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java))
-                    .hasSize(statusList.size)
+            // wrap in an async block so the test can advance the clock if throttling credential
+            // checks prevents the method from returning
+            val statusList = mutableListOf<CredentialStatus>()
+            interactor
+                .verifyCredential(
+                    pinRequest(credentialOwner),
+                    LockscreenCredential.createPin("1234"),
+                )
+                .toList(statusList)
 
-                verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout))
-            } else { // failed
-                assertThat(failedResult.error)
-                    .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern())
+            val last = statusList.removeLastOrNull()
+            if (result.isMatched) {
                 assertThat(statusList).isEmpty()
+                val successfulResult = last as? CredentialStatus.Success.Verified
+                assertThat(successfulResult).isNotNull()
+                assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT)
 
-                verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+                verify(lockPatternUtils).userPresent(eq(USER_ID))
+                verify(lockPatternUtils)
+                    .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle))
+            } else {
+                val failedResult = last as? CredentialStatus.Fail.Error
+                assertThat(failedResult).isNotNull()
+                assertThat(failedResult!!.remainingAttempts)
+                    .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1)
+                assertThat(failedResult.urgentMessage).isNull()
+
+                if (result.timeout > 0) { // failed and throttled
+                    // messages are in the throttled errors, so the final Error.error is empty
+                    assertThat(failedResult.error).isEmpty()
+                    assertThat(statusList).isNotEmpty()
+                    assertThat(
+                            statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java)
+                        )
+                        .hasSize(statusList.size)
+
+                    verify(lockPatternUtils)
+                        .setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout))
+                } else { // failed
+                    assertThat(failedResult.error)
+                        .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern())
+                    assertThat(statusList).isEmpty()
+
+                    verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+                }
             }
         }
-    }
 
     @Test
     fun pinCredentialWhenBadAndFinalAttempt() = runTest {
@@ -212,11 +244,11 @@
     }
 }
 
-private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
+private fun pinRequest(credentialOwner: Int = USER_ID): BiometricPromptRequest.Credential.Pin =
     BiometricPromptRequest.Credential.Pin(
         promptInfo(),
-        BiometricUserInfo(USER_ID),
-        BiometricOperationInfo(OPERATION_ID)
+        BiometricUserInfo(userId = USER_ID, deviceCredentialOwnerId = credentialOwner),
+        BiometricOperationInfo(OPERATION_ID),
     )
 
 private fun goodCredential(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
index dc499cd..b39a888 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -77,6 +77,7 @@
     private val fingerprintRepository = FakeFingerprintPropertyRepository()
     private val promptRepository = FakePromptRepository()
     private val fakeExecutor = FakeExecutor(FakeSystemClock())
+    private val credentialInteractor = FakeCredentialInteractor()
 
     private lateinit var displayStateRepository: FakeDisplayStateRepository
     private lateinit var displayRepository: FakeDisplayRepository
@@ -99,8 +100,9 @@
             PromptSelectorInteractorImpl(
                 fingerprintRepository,
                 displayStateInteractor,
+                credentialInteractor,
                 promptRepository,
-                lockPatternUtils
+                lockPatternUtils,
             )
     }
 
@@ -134,13 +136,13 @@
         testScope.runTest {
             useBiometricsAndReset(
                 allowCredentialFallback = true,
-                setComponentNameForConfirmDeviceCredentialActivity = true
+                setComponentNameForConfirmDeviceCredentialActivity = true,
             )
         }
 
     private fun TestScope.useBiometricsAndReset(
         allowCredentialFallback: Boolean,
-        setComponentNameForConfirmDeviceCredentialActivity: Boolean = false
+        setComponentNameForConfirmDeviceCredentialActivity: Boolean = false,
     ) {
         setUserCredentialType(isPassword = true)
 
@@ -357,7 +359,7 @@
 
     private fun setPrompt(
         info: PromptInfo = basicPromptInfo(),
-        onSwitchToCredential: Boolean = false
+        onSwitchToCredential: Boolean = false,
     ) {
         interactor.setPrompt(
             info,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 55fd344..c803097 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -28,7 +28,6 @@
 import android.graphics.Rect
 import android.graphics.drawable.BitmapDrawable
 import android.hardware.biometrics.BiometricFingerprintConstants
-import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT
 import android.hardware.biometrics.PromptContentItemBulletedText
 import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
@@ -163,7 +162,7 @@
             naturalDisplayWidth = 1000,
             naturalDisplayHeight = 3000,
             scaleFactor = 1f,
-            rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0
+            rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
         )
 
     private lateinit var promptContentView: PromptContentView
@@ -180,21 +179,21 @@
         whenever(
                 kosmos.packageManager.getApplicationInfo(
                     eq(OP_PACKAGE_NAME_WITH_APP_LOGO),
-                    anyInt()
+                    anyInt(),
                 )
             )
             .thenReturn(applicationInfoWithIconAndDescription)
         whenever(
                 kosmos.packageManager.getApplicationInfo(
                     eq(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
-                    anyInt()
+                    anyInt(),
                 )
             )
             .thenReturn(applicationInfoWithIconAndDescription)
         whenever(
                 kosmos.packageManager.getApplicationInfo(
                     eq(OP_PACKAGE_NAME_CAN_NOT_BE_FOUND),
-                    anyInt()
+                    anyInt(),
                 )
             )
             .thenThrow(NameNotFoundException())
@@ -220,13 +219,13 @@
         overrideResource(logoResFromApp, logoDrawableFromAppRes)
         overrideResource(
             R.array.config_useActivityLogoForBiometricPrompt,
-            arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO)
+            arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO),
         )
 
         overrideResource(R.dimen.biometric_dialog_fingerprint_icon_width, mockFingerprintIconWidth)
         overrideResource(
             R.dimen.biometric_dialog_fingerprint_icon_height,
-            mockFingerprintIconHeight
+            mockFingerprintIconHeight,
         )
         overrideResource(R.dimen.biometric_dialog_face_icon_size, mockFaceIconSize)
 
@@ -243,7 +242,7 @@
                 it.sensorType.toSensorType(),
                 it.allLocations.associateBy { sensorLocationInternal ->
                     sensorLocationInternal.displayId
-                }
+                },
             )
         }
 
@@ -441,7 +440,7 @@
 
                 kosmos.promptViewModel.showAuthenticated(
                     modality = testCase.authenticatedModality,
-                    dismissAfterDelay = DELAY
+                    dismissAfterDelay = DELAY,
                 )
 
                 // SFPS test cases
@@ -513,7 +512,7 @@
                 kosmos.promptViewModel.showAuthenticated(
                     modality = testCase.authenticatedModality,
                     dismissAfterDelay = DELAY,
-                    "TEST"
+                    "TEST",
                 )
 
                 if (testCase.isFingerprintOnly) {
@@ -558,7 +557,7 @@
 
                 kosmos.promptViewModel.showAuthenticated(
                     modality = testCase.authenticatedModality,
-                    dismissAfterDelay = DELAY
+                    dismissAfterDelay = DELAY,
                 )
 
                 if (testCase.isFaceOnly) {
@@ -598,7 +597,7 @@
 
                 kosmos.promptViewModel.showAuthenticated(
                     modality = testCase.authenticatedModality,
-                    dismissAfterDelay = DELAY
+                    dismissAfterDelay = DELAY,
                 )
 
                 kosmos.promptViewModel.confirmAuthenticated()
@@ -701,7 +700,7 @@
                     .isEqualTo(
                         Pair(
                             expectedUdfpsOverlayParams.sensorBounds.width(),
-                            expectedUdfpsOverlayParams.sensorBounds.height()
+                            expectedUdfpsOverlayParams.sensorBounds.height(),
                         )
                     )
             } else {
@@ -834,7 +833,7 @@
 
                 kosmos.promptViewModel.showAuthenticated(
                     modality = testCase.authenticatedModality,
-                    dismissAfterDelay = DELAY
+                    dismissAfterDelay = DELAY,
                 )
 
                 kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
@@ -907,10 +906,7 @@
                 }
             )
 
-        assertButtonsVisible(
-            cancel = expectConfirmation,
-            confirm = expectConfirmation,
-        )
+        assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation)
     }
 
     @Test
@@ -1158,10 +1154,7 @@
 
         testScheduler.runCurrent()
         assertThat(messages)
-            .containsExactly(
-                PromptMessage.Empty,
-                PromptMessage.Error(expectedErrorMessage),
-            )
+            .containsExactly(PromptMessage.Empty, PromptMessage.Error(expectedErrorMessage))
             .inOrder()
 
         testScheduler.advanceUntilIdle()
@@ -1221,10 +1214,7 @@
         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
         if (expectConfirmation) {
             assertThat(size).isEqualTo(PromptSize.MEDIUM)
-            assertButtonsVisible(
-                cancel = true,
-                confirm = true,
-            )
+            assertButtonsVisible(cancel = true, confirm = true)
 
             kosmos.promptViewModel.confirmAuthenticated()
             assertThat(message).isEqualTo(PromptMessage.Empty)
@@ -1251,10 +1241,7 @@
         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
         if (expectConfirmation) {
             assertThat(size).isEqualTo(PromptSize.MEDIUM)
-            assertButtonsVisible(
-                cancel = true,
-                confirm = true,
-            )
+            assertButtonsVisible(cancel = true, confirm = true)
 
             if (testCase.modalities.hasSfps) {
                 kosmos.promptViewModel.showAuthenticated(BiometricModality.Fingerprint, 0)
@@ -1290,10 +1277,7 @@
         if (expectConfirmation) {
             if (testCase.isFaceOnly) {
                 assertThat(size).isEqualTo(PromptSize.MEDIUM)
-                assertButtonsVisible(
-                    cancel = true,
-                    confirm = true,
-                )
+                assertButtonsVisible(cancel = true, confirm = true)
 
                 kosmos.promptViewModel.confirmAuthenticated()
             } else if (testCase.isCoex) {
@@ -1323,10 +1307,7 @@
         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
         if (expectConfirmation) {
             assertThat(size).isEqualTo(PromptSize.MEDIUM)
-            assertButtonsVisible(
-                cancel = true,
-                confirm = true,
-            )
+            assertButtonsVisible(cancel = true, confirm = true)
 
             kosmos.promptViewModel.confirmAuthenticated()
             assertThat(message).isEqualTo(PromptMessage.Empty)
@@ -1398,10 +1379,7 @@
         assertThat(authenticating).isFalse()
         assertThat(authenticated?.isAuthenticated).isTrue()
         assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
-        assertButtonsVisible(
-            cancel = expectConfirmation,
-            confirm = expectConfirmation,
-        )
+        assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation)
     }
 
     @Test
@@ -1421,7 +1399,7 @@
                 errorMessage,
                 messageAfterError = helpMessage,
                 authenticateAfterError = false,
-                failedModality = testCase.authenticatedModality
+                failedModality = testCase.authenticatedModality,
             )
         }
 
@@ -1472,7 +1450,7 @@
 
         kosmos.promptViewModel.onAnnounceAccessibilityHint(
             obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER),
-            true
+            true,
         )
 
         if (testCase.modalities.hasUdfps) {
@@ -1497,14 +1475,13 @@
 
         kosmos.promptViewModel.onAnnounceAccessibilityHint(
             obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER),
-            true
+            true,
         )
 
         assertThat(hint.isNullOrBlank()).isTrue()
     }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun descriptionOverriddenByVerticalListContentView() =
         runGenericTest(description = "test description", contentView = promptContentView) {
             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
@@ -1515,11 +1492,10 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun descriptionOverriddenByContentViewWithMoreOptionsButton() =
         runGenericTest(
             description = "test description",
-            contentView = promptContentViewWithMoreOptionsButton
+            contentView = promptContentViewWithMoreOptionsButton,
         ) {
             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
             val description by collectLastValue(kosmos.promptViewModel.description)
@@ -1529,7 +1505,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun descriptionWithoutContentView() =
         runGenericTest(description = "test description") {
             val contentView by collectLastValue(kosmos.promptViewModel.contentView)
@@ -1540,7 +1515,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_nullIfPkgNameNotFound() =
         runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1549,7 +1523,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_defaultFromActivityInfo() =
         runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1564,7 +1537,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_defaultIsNull() =
         runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1573,7 +1545,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_default() = runGenericTest {
         val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
         assertThat(logoInfo).isNotNull()
@@ -1581,7 +1552,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_resSetByApp() =
         runGenericTest(logoRes = logoResFromApp) {
             val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap()
@@ -1591,7 +1561,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logo_bitmapSetByApp() =
         runGenericTest(logoBitmap = logoBitmapFromApp) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1599,7 +1568,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logoDescription_emptyIfPkgNameNotFound() =
         runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1607,7 +1575,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logoDescription_defaultFromActivityInfo() =
         runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1619,7 +1586,6 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logoDescription_defaultIsEmpty() =
         runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1627,14 +1593,12 @@
         }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logoDescription_default() = runGenericTest {
         val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
         assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo)
     }
 
     @Test
-    @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
     fun logoDescription_setByApp() =
         runGenericTest(logoDescription = logoDescriptionFromApp) {
             val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1826,7 +1790,7 @@
         kosmos.biometricStatusRepository.setFingerprintAcquiredStatus(
             AcquiredFingerprintAuthenticationStatus(
                 AuthenticationReason.BiometricPromptAuthentication,
-                BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN
+                BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN,
             )
         )
 
@@ -1893,7 +1857,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON
+                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
@@ -1903,7 +1867,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON
+                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
@@ -1913,7 +1877,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+                                sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
@@ -1932,7 +1896,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON
+                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
@@ -1958,7 +1922,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON
+                                sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
@@ -1969,7 +1933,7 @@
                     fingerprint =
                         fingerprintSensorPropertiesInternal(
                                 strong = true,
-                                sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+                                sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
                             )
                             .first(),
                     authenticatedModality = BiometricModality.Fingerprint,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index 2b7e7ad..20d6615 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
@@ -16,34 +16,57 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import android.hardware.face.FaceManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.biometrics.authController
 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.bouncer.data.repository.keyguardBouncerRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.configureKeyguardBypass
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.data.repository.powerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.phone.dozeScrimController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -51,24 +74,52 @@
 class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.deviceEntryHapticsInteractor
 
+    private lateinit var underTest: DeviceEntryHapticsInteractor
+
+    @Before
+    fun setup() {
+        if (SceneContainerFlag.isEnabled) {
+            whenever(kosmos.authController.isUdfpsFingerDown).thenReturn(false)
+            whenever(kosmos.dozeScrimController.isPulsing).thenReturn(false)
+            whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+            whenever(kosmos.screenOffAnimationController.isKeyguardShowDelayed()).thenReturn(false)
+
+            // Dependencies for DeviceEntrySourceInteractor#biometricUnlockStateOnKeyguardDismissed
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+
+            // Mock authenticationMethodIsSecure true
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+
+            kosmos.keyguardBouncerRepository.setAlternateVisible(false)
+            kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+        } else {
+            underTest = kosmos.deviceEntryHapticsInteractor
+        }
+    }
+
+    @DisableSceneContainer
     @Test
     fun nonPowerButtonFPS_vibrateSuccess() =
         testScope.runTest {
             val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
-            setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC)
+            enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
             runCurrent()
-            enterDeviceFromBiometricUnlock()
+            enterDeviceFromFingerprintUnlockLegacy()
             assertThat(playSuccessHaptic).isNotNull()
         }
 
+    @DisableSceneContainer
     @Test
     fun powerButtonFPS_vibrateSuccess() =
         testScope.runTest {
             val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
-            setPowerButtonFingerprintProperty()
-            setFingerprintEnrolled()
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
             kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
 
             // It's been 10 seconds since the last power button wakeup
@@ -76,16 +127,16 @@
             advanceTimeBy(10000)
             runCurrent()
 
-            enterDeviceFromBiometricUnlock()
+            enterDeviceFromFingerprintUnlockLegacy()
             assertThat(playSuccessHaptic).isNotNull()
         }
 
+    @DisableSceneContainer
     @Test
     fun powerButtonFPS_powerDown_doNotVibrateSuccess() =
         testScope.runTest {
             val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
-            setPowerButtonFingerprintProperty()
-            setFingerprintEnrolled()
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
             kosmos.fakeKeyEventRepository.setPowerButtonDown(true) // power button is currently DOWN
 
             // It's been 10 seconds since the last power button wakeup
@@ -93,16 +144,16 @@
             advanceTimeBy(10000)
             runCurrent()
 
-            enterDeviceFromBiometricUnlock()
+            enterDeviceFromFingerprintUnlockLegacy()
             assertThat(playSuccessHaptic).isNull()
         }
 
+    @DisableSceneContainer
     @Test
     fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() =
         testScope.runTest {
             val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
-            setPowerButtonFingerprintProperty()
-            setFingerprintEnrolled()
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
             kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
 
             // It's only been 50ms since the last power button wakeup
@@ -110,7 +161,7 @@
             advanceTimeBy(50)
             runCurrent()
 
-            enterDeviceFromBiometricUnlock()
+            enterDeviceFromFingerprintUnlockLegacy()
             assertThat(playSuccessHaptic).isNull()
         }
 
@@ -118,7 +169,7 @@
     fun nonPowerButtonFPS_vibrateError() =
         testScope.runTest {
             val playErrorHaptic by collectLastValue(underTest.playErrorHaptic)
-            setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC)
+            enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
             runCurrent()
             fingerprintFailure()
             assertThat(playErrorHaptic).isNotNull()
@@ -128,8 +179,8 @@
     fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() =
         testScope.runTest {
             val playErrorHaptic by collectLastValue(underTest.playErrorHaptic)
-            setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC)
-            coExEnrolledAndEnabled()
+            enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
+            enrollFace()
             runCurrent()
             faceFailure()
             assertThat(playErrorHaptic).isNull()
@@ -139,8 +190,7 @@
     fun powerButtonFPS_vibrateError() =
         testScope.runTest {
             val playErrorHaptic by collectLastValue(underTest.playErrorHaptic)
-            setPowerButtonFingerprintProperty()
-            setFingerprintEnrolled()
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
             runCurrent()
             fingerprintFailure()
             assertThat(playErrorHaptic).isNotNull()
@@ -150,15 +200,143 @@
     fun powerButtonFPS_powerDown_doNotVibrateError() =
         testScope.runTest {
             val playErrorHaptic by collectLastValue(underTest.playErrorHaptic)
-            setPowerButtonFingerprintProperty()
-            setFingerprintEnrolled()
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
             kosmos.fakeKeyEventRepository.setPowerButtonDown(true)
             runCurrent()
             fingerprintFailure()
             assertThat(playErrorHaptic).isNull()
         }
 
-    private suspend fun enterDeviceFromBiometricUnlock() {
+    @EnableSceneContainer
+    @Test
+    fun playSuccessHaptic_onDeviceEntryFromUdfps_sceneContainer() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+            enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
+            runCurrent()
+            configureDeviceEntryFromBiometricSource(isFpUnlock = true)
+            verifyDeviceEntryFromFingerprintAuth()
+            assertThat(playSuccessHaptic).isNotNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun playSuccessHaptic_onDeviceEntryFromSfps_sceneContainer() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
+            kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
+
+            // It's been 10 seconds since the last power button wakeup
+            setAwakeFromPowerButton()
+            advanceTimeBy(10000)
+            runCurrent()
+
+            configureDeviceEntryFromBiometricSource(isFpUnlock = true)
+            verifyDeviceEntryFromFingerprintAuth()
+            assertThat(playSuccessHaptic).isNotNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun playSuccessHaptic_onDeviceEntryFromFaceAuth_sceneContainer() =
+        testScope.runTest {
+            enrollFace()
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+            configureDeviceEntryFromBiometricSource(isFaceUnlock = true)
+            verifyDeviceEntryFromFaceAuth()
+            assertThat(playSuccessHaptic).isNotNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
+            // power button is currently DOWN
+            kosmos.fakeKeyEventRepository.setPowerButtonDown(true)
+
+            // It's been 10 seconds since the last power button wakeup
+            setAwakeFromPowerButton()
+            advanceTimeBy(10000)
+            runCurrent()
+
+            configureDeviceEntryFromBiometricSource(isFpUnlock = true)
+            verifyDeviceEntryFromFingerprintAuth()
+            assertThat(playSuccessHaptic).isNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerButtonRecentlyPressed_sceneContainer() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+            enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
+            kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
+
+            // It's only been 50ms since the last power button wakeup
+            setAwakeFromPowerButton()
+            advanceTimeBy(50)
+            runCurrent()
+
+            configureDeviceEntryFromBiometricSource(isFpUnlock = true)
+            verifyDeviceEntryFromFingerprintAuth()
+            assertThat(playSuccessHaptic).isNull()
+        }
+
+    // Mock dependencies for DeviceEntrySourceInteractor#deviceEntryFromBiometricSource
+    private fun configureDeviceEntryFromBiometricSource(
+        isFpUnlock: Boolean = false,
+        isFaceUnlock: Boolean = false,
+    ) {
+        // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState
+        if (isFpUnlock) {
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+        }
+        if (isFaceUnlock) {
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                SuccessFaceAuthenticationStatus(
+                    FaceManager.AuthenticationResult(null, null, 0, true)
+                )
+            )
+
+            // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING
+            kosmos.sceneInteractor.setTransitionState(
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(Scenes.Lockscreen)
+                )
+            )
+        }
+        underTest = kosmos.deviceEntryHapticsInteractor
+    }
+
+    private fun TestScope.verifyDeviceEntryFromFingerprintAuth() {
+        val deviceEntryFromBiometricSource by
+            collectLastValue(kosmos.deviceEntrySourceInteractor.deviceEntryFromBiometricSource)
+        assertThat(deviceEntryFromBiometricSource)
+            .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+    }
+
+    private fun TestScope.verifyDeviceEntryFromFaceAuth() {
+        val deviceEntryFromBiometricSource by
+            collectLastValue(kosmos.deviceEntrySourceInteractor.deviceEntryFromBiometricSource)
+        assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR)
+    }
+
+    private fun enterDeviceFromFingerprintUnlockLegacy() {
         kosmos.fakeKeyguardRepository.setBiometricUnlockSource(
             BiometricUnlockSource.FINGERPRINT_SENSOR
         )
@@ -177,21 +355,22 @@
         )
     }
 
-    private fun setFingerprintSensorType(fingerprintSensorType: FingerprintSensorType) {
-        kosmos.fingerprintPropertyRepository.setProperties(
-            sensorId = 0,
-            strength = SensorStrength.STRONG,
-            sensorType = fingerprintSensorType,
-            sensorLocations = mapOf(),
-        )
+    private fun enrollFingerprint(fingerprintSensorType: FingerprintSensorType?) {
+        if (fingerprintSensorType == null) {
+            kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        } else {
+            kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fingerprintPropertyRepository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = fingerprintSensorType,
+                sensorLocations = mapOf(),
+            )
+        }
     }
 
-    private fun setPowerButtonFingerprintProperty() {
-        setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON)
-    }
-
-    private fun setFingerprintEnrolled() {
-        kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+    private fun enrollFace() {
+        kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
     }
 
     private fun setAwakeFromPowerButton() {
@@ -202,9 +381,4 @@
             powerButtonLaunchGestureTriggered = false,
         )
     }
-
-    private fun coExEnrolledAndEnabled() {
-        setFingerprintEnrolled()
-        kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt
index 2e4c97b..b3c891d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt
@@ -16,21 +16,51 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import android.hardware.face.FaceManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.biometrics.authController
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.configureKeyguardBypass
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.keyguardBypassRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.verifyCallback
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.phone.dozeScrimController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.statusbar.policy.devicePostureController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -38,45 +68,328 @@
 class DeviceEntrySourceInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val underTest = kosmos.deviceEntrySourceInteractor
+    private lateinit var underTest: DeviceEntrySourceInteractor
 
+    @Before
+    fun setup() {
+        if (SceneContainerFlag.isEnabled) {
+            whenever(kosmos.authController.isUdfpsFingerDown).thenReturn(false)
+            whenever(kosmos.dozeScrimController.isPulsing).thenReturn(false)
+            whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true)
+            whenever(kosmos.screenOffAnimationController.isKeyguardShowDelayed()).thenReturn(false)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+        } else {
+            underTest = kosmos.deviceEntrySourceInteractor
+        }
+    }
+
+    @DisableSceneContainer
     @Test
     fun deviceEntryFromFaceUnlock() =
         testScope.runTest {
             val deviceEntryFromBiometricAuthentication by
                 collectLastValue(underTest.deviceEntryFromBiometricSource)
-            keyguardRepository.setBiometricUnlockState(
+
+            kosmos.fakeKeyguardRepository.setBiometricUnlockState(
                 BiometricUnlockMode.WAKE_AND_UNLOCK,
                 BiometricUnlockSource.FACE_SENSOR,
             )
             runCurrent()
+
             assertThat(deviceEntryFromBiometricAuthentication)
                 .isEqualTo(BiometricUnlockSource.FACE_SENSOR)
         }
 
+    @DisableSceneContainer
     @Test
-    fun deviceEntryFromFingerprintUnlock() = runTest {
-        val deviceEntryFromBiometricAuthentication by
-            collectLastValue(underTest.deviceEntryFromBiometricSource)
-        keyguardRepository.setBiometricUnlockState(
-            BiometricUnlockMode.WAKE_AND_UNLOCK,
-            BiometricUnlockSource.FINGERPRINT_SENSOR,
-        )
-        runCurrent()
-        assertThat(deviceEntryFromBiometricAuthentication)
-            .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+    fun deviceEntryFromFingerprintUnlock() =
+        testScope.runTest {
+            val deviceEntryFromBiometricAuthentication by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            kosmos.fakeKeyguardRepository.setBiometricUnlockState(
+                BiometricUnlockMode.WAKE_AND_UNLOCK,
+                BiometricUnlockSource.FINGERPRINT_SENSOR,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricAuthentication)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @DisableSceneContainer
+    @Test
+    fun noDeviceEntry() =
+        testScope.runTest {
+            val deviceEntryFromBiometricAuthentication by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            kosmos.fakeKeyguardRepository.setBiometricUnlockState(
+                BiometricUnlockMode.ONLY_WAKE, // doesn't dismiss keyguard:
+                BiometricUnlockSource.FINGERPRINT_SENSOR,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricAuthentication).isNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFingerprintUnlockOnLockScreen_sceneContainerEnabled() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntrySourceInteractor
+
+            assertThat(deviceEntryFromBiometricSource)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFingerprintUnlockOnAod_sceneContainerEnabled() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(false)
+            configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true)
+            configureBiometricUnlockState(alternateBouncerVisible = false, sceneKey = Scenes.Dream)
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFingerprintUnlockOnBouncer_sceneContainerEnabled() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Bouncer,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFingerprintUnlockOnShade_sceneContainerEnabled() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFingerprintUnlockOnAlternateBouncer_sceneContainerEnabled() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = true,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource)
+                .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnLockScreen_bypassAvailable_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntrySourceInteractor
+
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnLockScreen_bypassDisabled_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntrySourceInteractor
+
+            collectLastValue(kosmos.keyguardBypassRepository.isBypassAvailable)
+            runCurrent()
+
+            val postureControllerCallback = kosmos.devicePostureController.verifyCallback()
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            // MODE_NONE does not dismiss keyguard
+            assertThat(deviceEntryFromBiometricSource).isNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnBouncer_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Bouncer,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR)
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnShade_bypassAvailable_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(alternateBouncerVisible = false, sceneKey = Scenes.Shade)
+            runCurrent()
+
+            // MODE_NONE does not dismiss keyguard
+            assertThat(deviceEntryFromBiometricSource).isNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnShade_bypassDisabled_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = false,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource).isNull()
+        }
+
+    @EnableSceneContainer
+    @Test
+    fun deviceEntryFromFaceUnlockOnAlternateBouncer_sceneContainerEnabled() =
+        testScope.runTest {
+            kosmos.configureKeyguardBypass(isBypassAvailable = true)
+            underTest = kosmos.deviceEntrySourceInteractor
+            val deviceEntryFromBiometricSource by
+                collectLastValue(underTest.deviceEntryFromBiometricSource)
+
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true)
+            configureBiometricUnlockState(
+                alternateBouncerVisible = true,
+                sceneKey = Scenes.Lockscreen,
+            )
+            runCurrent()
+
+            assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR)
+        }
+
+    private fun configureDeviceEntryBiometricAuthSuccessState(
+        isFingerprintAuth: Boolean = false,
+        isFaceAuth: Boolean = false,
+    ) {
+        if (isFingerprintAuth) {
+            val successStatus = SuccessFingerprintAuthenticationStatus(0, true)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(successStatus)
+        }
+
+        if (isFaceAuth) {
+            val successStatus: FaceAuthenticationStatus =
+                SuccessFaceAuthenticationStatus(
+                    FaceManager.AuthenticationResult(null, null, 0, true)
+                )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(successStatus)
+        }
     }
 
-    @Test
-    fun noDeviceEntry() = runTest {
-        val deviceEntryFromBiometricAuthentication by
-            collectLastValue(underTest.deviceEntryFromBiometricSource)
-        keyguardRepository.setBiometricUnlockState(
-            BiometricUnlockMode.ONLY_WAKE, // doesn't dismiss keyguard:
-            BiometricUnlockSource.FINGERPRINT_SENSOR,
+    private fun configureBiometricUnlockState(
+        alternateBouncerVisible: Boolean,
+        sceneKey: SceneKey,
+    ) {
+        kosmos.keyguardBouncerRepository.setAlternateVisible(alternateBouncerVisible)
+        kosmos.sceneInteractor.changeScene(sceneKey, "reason")
+        kosmos.sceneInteractor.setTransitionState(
+            MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(sceneKey))
         )
-        runCurrent()
-        assertThat(deviceEntryFromBiometricAuthentication).isNull()
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
index 77337d3..a981e20 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.content.mockedContext
+import android.content.res.Resources
 import android.hardware.fingerprint.FingerprintManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -41,13 +42,16 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -55,6 +59,7 @@
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -63,8 +68,8 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.occludingAppDeviceEntryInteractor
-
+    private lateinit var underTest: OccludingAppDeviceEntryInteractor
+    private lateinit var mockedResources: Resources
     private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
     private val keyguardRepository = kosmos.fakeKeyguardRepository
     private val bouncerRepository = kosmos.keyguardBouncerRepository
@@ -74,9 +79,18 @@
     private val mockedContext = kosmos.mockedContext
     private val mockedActivityStarter = kosmos.activityStarter
 
+    @Before
+    fun setup() {
+        mockedResources = mock<Resources>()
+        whenever(mockedContext.resources).thenReturn(mockedResources)
+        whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps))
+            .thenReturn(true)
+    }
+
     @Test
     fun fingerprintSuccess_goToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(true)
             fingerprintAuthRepository.setAuthenticationStatus(
                 SuccessFingerprintAuthenticationStatus(0, true)
@@ -86,8 +100,23 @@
         }
 
     @Test
+    fun fingerprintSuccess_configOff_doesNotGoToHomeScreen() =
+        testScope.runTest {
+            whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps))
+                .thenReturn(false)
+            underTest = kosmos.occludingAppDeviceEntryInteractor
+            givenOnOccludingApp(true)
+            fingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            verifyNeverGoToHomeScreen()
+        }
+
+    @Test
     fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(true)
             powerRepository.setInteractive(false)
             fingerprintAuthRepository.setAuthenticationStatus(
@@ -100,6 +129,7 @@
     @Test
     fun fingerprintSuccess_dreaming_doesNotGoToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(true)
             keyguardRepository.setDreaming(true)
             fingerprintAuthRepository.setAuthenticationStatus(
@@ -112,6 +142,7 @@
     @Test
     fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(false)
             fingerprintAuthRepository.setAuthenticationStatus(
                 SuccessFingerprintAuthenticationStatus(0, true)
@@ -123,11 +154,12 @@
     @Test
     fun lockout_goToHomeScreenOnDismissAction() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(true)
             fingerprintAuthRepository.setAuthenticationStatus(
                 ErrorFingerprintAuthenticationStatus(
                     FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
-                    "lockoutTest"
+                    "lockoutTest",
                 )
             )
             runCurrent()
@@ -137,11 +169,12 @@
     @Test
     fun lockout_notOnOccludingApp_neverGoToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(false)
             fingerprintAuthRepository.setAuthenticationStatus(
                 ErrorFingerprintAuthenticationStatus(
                     FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
-                    "lockoutTest"
+                    "lockoutTest",
                 )
             )
             runCurrent()
@@ -151,11 +184,12 @@
     @Test
     fun lockout_onOccludingApp_onCommunal_neverGoToHomeScreen() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             givenOnOccludingApp(isOnOccludingApp = true, isOnCommunal = true)
             fingerprintAuthRepository.setAuthenticationStatus(
                 ErrorFingerprintAuthenticationStatus(
                     FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
-                    "lockoutTest"
+                    "lockoutTest",
                 )
             )
             runCurrent()
@@ -165,6 +199,7 @@
     @Test
     fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             val message by collectLastValue(underTest.message)
 
             givenOnOccludingApp(true)
@@ -186,6 +221,7 @@
     @Test
     fun message_fpErrorHelpFailOnOccludingApp() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             val message by collectLastValue(underTest.message)
 
             givenOnOccludingApp(true)
@@ -218,6 +254,7 @@
     @Test
     fun message_fpError_lockoutFilteredOut() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             val message by collectLastValue(underTest.message)
 
             givenOnOccludingApp(true)
@@ -246,6 +283,7 @@
     @Test
     fun noMessage_fpErrorsWhileDozing() =
         testScope.runTest {
+            underTest = kosmos.occludingAppDeviceEntryInteractor
             val message by collectLastValue(underTest.message)
 
             givenOnOccludingApp(true)
@@ -254,7 +292,7 @@
             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.OCCLUDED,
                 to = KeyguardState.DOZING,
-                testScope
+                testScope,
             )
             runCurrent()
 
@@ -283,7 +321,7 @@
 
     private suspend fun givenOnOccludingApp(
         isOnOccludingApp: Boolean,
-        isOnCommunal: Boolean = false
+        isOnCommunal: Boolean = false,
     ) {
         powerRepository.setInteractive(true)
         keyguardRepository.setIsDozing(false)
@@ -305,13 +343,13 @@
             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.OCCLUDED,
-                testScope
+                testScope,
             )
         } else {
             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.OCCLUDED,
                 to = KeyguardState.LOCKSCREEN,
-                testScope
+                testScope,
             )
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
index 9300db9..4317b9f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -16,25 +16,37 @@
 package com.android.systemui.dreams.homecontrols
 
 import android.app.Activity
+import android.content.ComponentName
 import android.content.Intent
+import android.os.powerManager
 import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL
 import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM
 import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE
+import android.service.dreams.DreamService
 import android.window.TaskFragmentInfo
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dreams.homecontrols.shared.model.fakeHomeControlsDataSource
+import com.android.systemui.dreams.homecontrols.shared.model.homeControlsDataSource
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
 import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
-import java.util.Optional
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,7 +59,6 @@
 import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -62,13 +73,18 @@
         WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) }
     }
 
+    private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher)
+
     private val taskFragmentComponent = mock<TaskFragmentComponent>()
     private val activity = mock<Activity>()
     private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
     private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
     private val hideCallback = argumentCaptor<() -> Unit>()
-    private val dreamServiceDelegate =
-        mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity }
+    private var dreamService =
+        mock<DreamService> {
+            on { activity } doReturn activity
+            on { redirectWake } doReturn false
+        }
 
     private val taskFragmentComponentFactory =
         mock<TaskFragmentComponent.Factory> {
@@ -82,12 +98,32 @@
             } doReturn taskFragmentComponent
         }
 
-    private val underTest: HomeControlsDreamService by lazy { buildService() }
+    private val underTest: HomeControlsDreamServiceImpl by lazy {
+        with(kosmos) {
+            HomeControlsDreamServiceImpl(
+                taskFragmentFactory = taskFragmentComponentFactory,
+                wakeLockBuilder = fakeWakeLockBuilder,
+                powerManager = powerManager,
+                systemClock = fakeSystemClock,
+                dataSource = homeControlsDataSource,
+                logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest"),
+                service = dreamService,
+                lifecycleOwner = lifecycleOwner,
+            )
+        }
+    }
 
     @Before
     fun setup() {
-        whenever(kosmos.controlsComponent.getControlsListingController())
-            .thenReturn(Optional.of(kosmos.controlsListingController))
+        Dispatchers.setMain(kosmos.testDispatcher)
+        kosmos.fakeHomeControlsDataSource.setComponentInfo(
+            HomeControlsComponentInfo(PANEL_COMPONENT, true)
+        )
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
     }
 
     @Test
@@ -108,13 +144,10 @@
     @Test
     fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() =
         testScope.runTest {
-            val serviceWithNullActivity =
-                buildService(
-                    mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null }
-                )
-
-            serviceWithNullActivity.onAttachedToWindow()
+            dreamService = mock<DreamService> { on { activity } doReturn null }
+            underTest.onAttachedToWindow()
             verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+            verify(dreamService).finish()
         }
 
     @Test
@@ -137,9 +170,9 @@
     @Test
     fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() =
         testScope.runTest {
-            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false)
             underTest.onAttachedToWindow()
             onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
+            runCurrent()
             verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
 
             // Task fragment becomes empty
@@ -149,16 +182,21 @@
             advanceUntilIdle()
             // Dream is finished and activity is not restarted
             verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
-            verify(dreamServiceDelegate, never()).wakeUp(any())
-            verify(dreamServiceDelegate).finish(any())
+            verify(dreamService, never()).wakeUp()
+            verify(dreamService).finish()
         }
 
     @Test
     fun testRestartsActivityWhenRedirectingWakes() =
         testScope.runTest {
-            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true)
+            dreamService =
+                mock<DreamService> {
+                    on { activity } doReturn activity
+                    on { redirectWake } doReturn true
+                }
             underTest.onAttachedToWindow()
             onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
+            runCurrent()
             verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
 
             // Task fragment becomes empty
@@ -166,30 +204,20 @@
                 mock<TaskFragmentInfo> { on { isEmpty } doReturn true }
             )
             advanceUntilIdle()
+
             // Activity is restarted instead of finishing the dream.
             verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher())
-            verify(dreamServiceDelegate).wakeUp(any())
-            verify(dreamServiceDelegate, never()).finish(any())
+            verify(dreamService).wakeUp()
+            verify(dreamService, never()).finish()
         }
 
     private fun intentMatcher() =
         argThat<Intent> {
             getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) ==
-                CONTROLS_SURFACE_DREAM
+                CONTROLS_SURFACE_DREAM && component == PANEL_COMPONENT
         }
 
-    private fun buildService(
-        activityProvider: DreamServiceDelegate = dreamServiceDelegate
-    ): HomeControlsDreamService =
-        with(kosmos) {
-            return HomeControlsDreamService(
-                controlsSettingsRepository = FakeControlsSettingsRepository(),
-                taskFragmentFactory = taskFragmentComponentFactory,
-                homeControlsComponentInteractor = homeControlsComponentInteractor,
-                wakeLockBuilder = fakeWakeLockBuilder,
-                dreamServiceDelegate = activityProvider,
-                bgDispatcher = testDispatcher,
-                logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest")
-            )
-        }
+    private companion object {
+        val PANEL_COMPONENT = ComponentName("test.pkg", "test.panel")
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
index 1adf414..ed45e8c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -34,9 +34,14 @@
 import com.android.systemui.controls.panels.SelectedComponentRepository
 import com.android.systemui.controls.panels.authorizedPanelsRepository
 import com.android.systemui.controls.panels.selectedComponentRepository
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.dreams.homecontrols.system.HomeControlsDreamStartable
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.userTracker
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
@@ -96,8 +101,9 @@
             HomeControlsDreamStartable(
                 mContext,
                 packageManager,
+                kosmos.userTracker,
                 homeControlsComponentInteractor,
-                kosmos.applicationCoroutineScope
+                kosmos.applicationCoroutineScope,
             )
     }
 
@@ -113,7 +119,7 @@
                 .setComponentEnabledSetting(
                     eq(componentName),
                     eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
-                    eq(PackageManager.DONT_KILL_APP)
+                    eq(PackageManager.DONT_KILL_APP),
                 )
         }
 
@@ -128,7 +134,7 @@
                 .setComponentEnabledSetting(
                     eq(componentName),
                     eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
-                    eq(PackageManager.DONT_KILL_APP)
+                    eq(PackageManager.DONT_KILL_APP),
                 )
         }
 
@@ -143,14 +149,14 @@
                 .setComponentEnabledSetting(
                     eq(componentName),
                     eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
-                    eq(PackageManager.DONT_KILL_APP)
+                    eq(PackageManager.DONT_KILL_APP),
                 )
         }
 
     private fun ControlsServiceInfo(
         componentName: ComponentName,
         label: CharSequence,
-        hasPanel: Boolean
+        hasPanel: Boolean,
     ): ControlsServiceInfo {
         val serviceInfo =
             ServiceInfo().apply {
@@ -165,7 +171,7 @@
         context: Context,
         serviceInfo: ServiceInfo,
         private val label: CharSequence,
-        hasPanel: Boolean
+        hasPanel: Boolean,
     ) : ControlsServiceInfo(context, serviceInfo) {
 
         init {
@@ -185,7 +191,7 @@
             UserInfo(
                 /* id= */ PRIMARY_USER_ID,
                 /* name= */ "primary user",
-                /* flags= */ UserInfo.FLAG_PRIMARY
+                /* flags= */ UserInfo.FLAG_PRIMARY,
             )
         private const val TEST_PACKAGE_PANEL = "pkg.panel"
         private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
@@ -193,13 +199,13 @@
             SelectedComponentRepository.SelectedComponent(
                 TEST_PACKAGE_PANEL,
                 TEST_COMPONENT_PANEL,
-                true
+                true,
             )
         private val TEST_SELECTED_COMPONENT_NON_PANEL =
             SelectedComponentRepository.SelectedComponent(
                 TEST_PACKAGE_PANEL,
                 TEST_COMPONENT_PANEL,
-                false
+                false,
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt
new file mode 100644
index 0000000..e57776f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+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.launch
+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 HomeControlsRemoteProxyTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder
+
+    private val underTest by lazy { kosmos.homeControlsRemoteProxy }
+
+    @Test
+    fun testRegistersOnlyWhileSubscribed() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            val job = launch { underTest.componentInfo.collect {} }
+            runCurrent()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+
+            job.cancel()
+            runCurrent()
+            assertThat(fakeBinder.callbacks).isEmpty()
+        }
+
+    @Test
+    fun testEmitsOnCallback() =
+        testScope.runTest {
+            val componentInfo by collectLastValue(underTest.componentInfo)
+            assertThat(componentInfo).isNull()
+
+            fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true)
+            assertThat(componentInfo)
+                .isEqualTo(
+                    HomeControlsComponentInfo(
+                        TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = true,
+                    )
+                )
+        }
+
+    @Test
+    fun testOnlyRegistersSingleCallbackForMultipleSubscribers() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            // 2 collectors
+            val job = launch {
+                launch { underTest.componentInfo.collect {} }
+                launch { underTest.componentInfo.collect {} }
+            }
+            runCurrent()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+            job.cancel()
+        }
+
+    private companion object {
+        val TEST_COMPONENT = ComponentName("pkg.test", "class.test")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt
new file mode 100644
index 0000000..4002175
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.PersistentConnectionManager
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RemoteHomeControlsDataSourceDelegatorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val proxy = kosmos.homeControlsRemoteProxy
+    private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder
+
+    private val callbackCaptor =
+        argumentCaptor<ObservableServiceConnection.Callback<HomeControlsRemoteProxy>>()
+
+    private val connectionManager =
+        mock<PersistentConnectionManager<HomeControlsRemoteProxy>> {
+            on { start() } doAnswer { simulateConnect() }
+            on { stop() } doAnswer { simulateDisconnect() }
+        }
+    private val serviceComponent =
+        mock<HomeControlsRemoteServiceComponent> {
+            on { connectionManager } doReturn connectionManager
+        }
+
+    private val underTest by lazy { kosmos.remoteHomeControlsDataSourceDelegator }
+
+    @Before
+    fun setUp() {
+        kosmos.homeControlsRemoteServiceFactory =
+            mock<HomeControlsRemoteServiceComponent.Factory>().stub {
+                on { create(callbackCaptor.capture()) } doReturn serviceComponent
+            }
+    }
+
+    @Test
+    fun testQueriesComponentInfoFromBinder() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            val componentInfo by collectLastValue(underTest.componentInfo)
+
+            assertThat(componentInfo).isNull()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+
+            fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true)
+            assertThat(componentInfo)
+                .isEqualTo(
+                    HomeControlsComponentInfo(
+                        TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = true,
+                    )
+                )
+        }
+
+    @Test
+    fun testOnlyConnectToServiceOnSubscription() =
+        testScope.runTest {
+            verify(connectionManager, never()).start()
+
+            val job = launch { underTest.componentInfo.collect {} }
+            runCurrent()
+            verify(connectionManager, times(1)).start()
+            verify(connectionManager, never()).stop()
+
+            job.cancel()
+            runCurrent()
+            verify(connectionManager, times(1)).start()
+            verify(connectionManager, times(1)).stop()
+        }
+
+    private fun simulateConnect() {
+        callbackCaptor.lastValue.onConnected(mock(), proxy)
+    }
+
+    private fun simulateDisconnect() {
+        callbackCaptor.lastValue.onDisconnected(mock(), 0)
+    }
+
+    private companion object {
+        val TEST_COMPONENT = ComponentName("pkg.test", "class.test")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt
new file mode 100644
index 0000000..f8a45e8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.dreams.homecontrols.system
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.authorizedPanelsRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsRemoteServiceBinderTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher)
+    private val fakeControlsSettingsRepository = FakeControlsSettingsRepository()
+
+    private val underTest by lazy {
+        HomeControlsRemoteServiceBinder(
+            kosmos.homeControlsComponentInteractor,
+            fakeControlsSettingsRepository,
+            kosmos.backgroundCoroutineContext,
+            logcatLogBuffer(),
+            lifecycleOwner,
+        )
+    }
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
+            whenever(controlsComponent.getControlsListingController())
+                .thenReturn(Optional.of(controlsListingController))
+        }
+    }
+
+    @Test
+    fun testRegisterSingleListener() =
+        testScope.runTest {
+            setup()
+            val controlsSettings by collectLastValue(addCallback())
+            runServicesUpdate()
+
+            assertThat(controlsSettings)
+                .isEqualTo(
+                    CallbackArgs(
+                        panelComponent = TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = false,
+                    )
+                )
+        }
+
+    @Test
+    fun testRegisterMultipleListeners() =
+        testScope.runTest {
+            setup()
+            val controlsSettings1 by collectLastValue(addCallback())
+            val controlsSettings2 by collectLastValue(addCallback())
+            runServicesUpdate()
+
+            assertThat(controlsSettings1)
+                .isEqualTo(
+                    CallbackArgs(
+                        panelComponent = TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = false,
+                    )
+                )
+            assertThat(controlsSettings2)
+                .isEqualTo(
+                    CallbackArgs(
+                        panelComponent = TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = false,
+                    )
+                )
+        }
+
+    @Test
+    fun testListenerCalledWhenStateChanges() =
+        testScope.runTest {
+            setup()
+            val controlsSettings by collectLastValue(addCallback())
+            runServicesUpdate()
+
+            assertThat(controlsSettings)
+                .isEqualTo(
+                    CallbackArgs(
+                        panelComponent = TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = false,
+                    )
+                )
+
+            kosmos.authorizedPanelsRepository.removeAuthorizedPanels(setOf(TEST_PACKAGE))
+
+            // Updated with null component now that we are no longer authorized.
+            assertThat(controlsSettings)
+                .isEqualTo(
+                    CallbackArgs(panelComponent = null, allowTrivialControlsOnLockscreen = false)
+                )
+        }
+
+    private fun TestScope.runServicesUpdate() {
+        runCurrent()
+        val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
+        val callback = withArgCaptor {
+            Mockito.verify(kosmos.controlsListingController).addCallback(capture())
+        }
+        callback.onServicesUpdated(listings)
+        runCurrent()
+    }
+
+    private fun addCallback() = conflatedCallbackFlow {
+        val callback =
+            object : IOnControlsSettingsChangeListener.Stub() {
+                override fun onControlsSettingsChanged(
+                    panelComponent: ComponentName?,
+                    allowTrivialControlsOnLockscreen: Boolean,
+                ) {
+                    trySend(CallbackArgs(panelComponent, allowTrivialControlsOnLockscreen))
+                }
+            }
+        underTest.registerListenerForCurrentUser(callback)
+        awaitClose { underTest.unregisterListenerForCurrentUser(callback) }
+    }
+
+    private suspend fun TestScope.setup() {
+        kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+        kosmos.fakeUserTracker.set(listOf(PRIMARY_USER), 0)
+        kosmos.authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+        kosmos.selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+        runCurrent()
+    }
+
+    private data class CallbackArgs(
+        val panelComponent: ComponentName?,
+        val allowTrivialControlsOnLockscreen: Boolean,
+    )
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean,
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean,
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    private companion object {
+        const val PRIMARY_USER_ID = 0
+        val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY,
+            )
+
+        private const val TEST_PACKAGE = "pkg"
+        private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, true)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt
similarity index 64%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt
index 7292985..c950523 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt
@@ -20,40 +20,32 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.ServiceInfo
 import android.content.pm.UserInfo
-import android.os.PowerManager
-import android.os.UserHandle
-import android.os.powerManager
-import android.service.dream.dreamManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.data.repository.fakePackageChangeRepository
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.panels.SelectedComponentRepository
 import com.android.systemui.controls.panels.authorizedPanelsRepository
 import com.android.systemui.controls.panels.selectedComponentRepository
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
-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.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mockito.anyLong
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -68,7 +60,6 @@
     @Before
     fun setUp() =
         with(kosmos) {
-            fakeSystemClock.setCurrentTimeMillis(0)
             fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
             whenever(controlsComponent.getControlsListingController())
                 .thenReturn(Optional.of(controlsListingController))
@@ -172,113 +163,6 @@
             }
         }
 
-    @Test
-    fun testMonitoringUpdatesAndRestart() =
-        with(kosmos) {
-            testScope.runTest {
-                setActiveUser(PRIMARY_USER)
-                authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
-                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
-                whenever(controlsListingController.getCurrentServices())
-                    .thenReturn(
-                        listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
-                    )
-
-                val job = launch { underTest.monitorUpdatesAndRestart() }
-                val panelComponent by collectLastValue(underTest.panelComponent)
-
-                assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
-                verify(dreamManager, never()).startDream()
-
-                fakeSystemClock.advanceTime(100)
-                // The package update is started.
-                fakePackageChangeRepository.notifyUpdateStarted(
-                    TEST_PACKAGE,
-                    UserHandle.of(PRIMARY_USER_ID),
-                )
-                fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds)
-                // Task fragment becomes empty as a result of the update.
-                underTest.onDreamEndUnexpectedly()
-
-                runCurrent()
-                verify(dreamManager, never()).startDream()
-
-                fakeSystemClock.advanceTime(500)
-                // The package update is finished.
-                fakePackageChangeRepository.notifyUpdateFinished(
-                    TEST_PACKAGE,
-                    UserHandle.of(PRIMARY_USER_ID),
-                )
-
-                runCurrent()
-                verify(dreamManager).startDream()
-                job.cancel()
-            }
-        }
-
-    @Test
-    fun testMonitoringUpdatesAndRestart_dreamEndsAfterDelay() =
-        with(kosmos) {
-            testScope.runTest {
-                setActiveUser(PRIMARY_USER)
-                authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE))
-                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
-                whenever(controlsListingController.getCurrentServices())
-                    .thenReturn(
-                        listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true))
-                    )
-
-                val job = launch { underTest.monitorUpdatesAndRestart() }
-                val panelComponent by collectLastValue(underTest.panelComponent)
-
-                assertThat(panelComponent).isEqualTo(TEST_COMPONENT)
-                verify(dreamManager, never()).startDream()
-
-                fakeSystemClock.advanceTime(100)
-                // The package update is started.
-                fakePackageChangeRepository.notifyUpdateStarted(
-                    TEST_PACKAGE,
-                    UserHandle.of(PRIMARY_USER_ID),
-                )
-                fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds + 100)
-                // Task fragment becomes empty as a result of the update.
-                underTest.onDreamEndUnexpectedly()
-
-                runCurrent()
-                verify(dreamManager, never()).startDream()
-
-                fakeSystemClock.advanceTime(500)
-                // The package update is finished.
-                fakePackageChangeRepository.notifyUpdateFinished(
-                    TEST_PACKAGE,
-                    UserHandle.of(PRIMARY_USER_ID),
-                )
-
-                runCurrent()
-                verify(dreamManager, never()).startDream()
-                job.cancel()
-            }
-        }
-
-    @Test
-    fun testDreamUnexpectedlyEnds_triggersUserActivity() =
-        with(kosmos) {
-            testScope.runTest {
-                fakeSystemClock.setUptimeMillis(100000L)
-                verify(powerManager, never()).userActivity(anyLong(), anyInt(), anyInt())
-
-                // Dream ends unexpectedly
-                underTest.onDreamEndUnexpectedly()
-
-                verify(powerManager)
-                    .userActivity(
-                        100000L,
-                        PowerManager.USER_ACTIVITY_EVENT_OTHER,
-                        PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS
-                    )
-            }
-        }
-
     private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
         val listings =
             listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean))
@@ -297,7 +181,7 @@
     private fun ControlsServiceInfo(
         componentName: ComponentName,
         label: CharSequence,
-        hasPanel: Boolean
+        hasPanel: Boolean,
     ): ControlsServiceInfo {
         val serviceInfo =
             ServiceInfo().apply {
@@ -312,7 +196,7 @@
         context: Context,
         serviceInfo: ServiceInfo,
         private val label: CharSequence,
-        hasPanel: Boolean
+        hasPanel: Boolean,
     ) : ControlsServiceInfo(context, serviceInfo) {
 
         init {
@@ -332,7 +216,7 @@
             UserInfo(
                 /* id= */ PRIMARY_USER_ID,
                 /* name= */ "primary user",
-                /* flags= */ UserInfo.FLAG_PRIMARY
+                /* flags= */ UserInfo.FLAG_PRIMARY,
             )
 
         private const val ANOTHER_USER_ID = 1
@@ -340,7 +224,7 @@
             UserInfo(
                 /* id= */ ANOTHER_USER_ID,
                 /* name= */ "another user",
-                /* flags= */ UserInfo.FLAG_PRIMARY
+                /* flags= */ UserInfo.FLAG_PRIMARY,
             )
         private const val TEST_PACKAGE = "pkg"
         private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index 9ca3ce6..e9e3e1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -81,7 +81,7 @@
             this.fakeKeyguardTransitionRepository =
                 FakeKeyguardTransitionRepository(
                     // This test sends transition steps manually in the test cases.
-                    sendTransitionStepsOnStartTransition = false,
+                    initiallySendTransitionStepsOnStartTransition = false,
                     testScope = testScope,
                 )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index 9c2e631..b29a5f4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -27,15 +27,13 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
-import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shade.data.repository.FlingInfo
 import com.android.systemui.shade.data.repository.fakeShadeRepository
@@ -48,6 +46,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -55,7 +55,9 @@
 class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
-            this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository(
+                testScope = testScope,
+            ))
         }
 
     private val testScope = kosmos.testScope
@@ -66,7 +68,7 @@
 
     @Before
     fun setup() {
-        transitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy
+        transitionRepository = kosmos.fakeKeyguardTransitionRepository
     }
 
     @Test
@@ -302,4 +304,74 @@
                     to = KeyguardState.LOCKSCREEN,
                 )
         }
+
+    /**
+     * External signals can cause us to transition from PRIMARY_BOUNCER -> * while a manual
+     * transition is in progress. This test was added after a bug that caused the manual transition
+     * ID to get stuck in this scenario, preventing subsequent transitions to PRIMARY_BOUNCER.
+     */
+    @Test
+    fun testExternalTransitionAwayFromBouncer_transitionIdNotStuck() =
+        testScope.runTest {
+            underTest.start()
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            keyguardRepository.setKeyguardDismissible(false)
+            shadeRepository.setLegacyShadeTracking(true)
+            keyguardRepository.setKeyguardOccluded(false)
+            runCurrent()
+
+            reset(transitionRepository)
+
+            // Disable automatic sending of transition steps so we can send steps through RUNNING
+            // to simulate a cancellation.
+            transitionRepository.sendTransitionStepsOnStartTransition = false
+            shadeRepository.setLegacyShadeExpansion(0.5f)
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                )
+
+            // Partially transition to PRIMARY_BOUNCER.
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                throughTransitionState = TransitionState.RUNNING,
+                testScope = testScope,
+            )
+
+            // Start a transition to GONE, which will cancel LS -> BOUNCER.
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.PRIMARY_BOUNCER,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+
+            // Go to AOD, then LOCKSCREEN.
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+
+            reset(transitionRepository)
+
+            // Start a swipe up to the bouncer, and verify that we started a transition to
+            // PRIMARY_BOUNCER, verifying the transition ID did not get stuck.
+            shadeRepository.setLegacyShadeExpansion(0.25f)
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                )
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index a8bb2b0..46d1ebe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dock.DockManagerFake
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
@@ -54,6 +55,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.cameraLauncher
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -117,8 +119,8 @@
                     BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
                     ":" +
-                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-            )
+                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET,
+            ),
         )
 
         repository = FakeKeyguardRepository()
@@ -141,13 +143,7 @@
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
-                        whenever(
-                                getSharedPreferences(
-                                    anyString(),
-                                    anyInt(),
-                                    anyInt(),
-                                )
-                            )
+                        whenever(getSharedPreferences(anyString(), anyInt(), anyInt()))
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
@@ -181,10 +177,7 @@
         featureFlags = FakeFeatureFlags()
 
         val withDeps =
-            KeyguardInteractorFactory.create(
-                featureFlags = featureFlags,
-                repository = repository,
-            )
+            KeyguardInteractorFactory.create(featureFlags = featureFlags, repository = repository)
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = withDeps.keyguardInteractor,
@@ -205,6 +198,7 @@
                 appContext = context,
                 sceneInteractor = { kosmos.sceneInteractor },
             )
+        kosmos.keyguardQuickAffordanceInteractor = underTest
 
         whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f))
     }
@@ -241,9 +235,7 @@
         testScope.runTest {
             val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
             quickAccessWallet.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue =
@@ -268,9 +260,7 @@
             whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
                 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL)
             quickAccessWallet.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue by
@@ -287,9 +277,7 @@
             whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
                 .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL)
             quickAccessWallet.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue by
@@ -305,9 +293,7 @@
         testScope.runTest {
             biometricSettingsRepository.setIsUserInLockdown(true)
             quickAccessWallet.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue by
@@ -323,9 +309,7 @@
         testScope.runTest {
             repository.setIsDozing(true)
             homeControls.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue =
@@ -340,9 +324,7 @@
         testScope.runTest {
             repository.setKeyguardShowing(false)
             homeControls.setState(
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = ICON,
-                )
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
             val collectedValue =
@@ -446,7 +428,7 @@
             val collectedValue =
                 collectLastValue(
                     underTest.quickAffordanceAlwaysVisible(
-                        KeyguardQuickAffordancePosition.BOTTOM_START,
+                        KeyguardQuickAffordancePosition.BOTTOM_START
                     )
                 )
             assertThat(collectedValue())
@@ -487,10 +469,7 @@
     @Test
     fun select() =
         testScope.runTest {
-            overrideResource(
-                R.array.config_keyguardQuickAffordanceDefaults,
-                arrayOf<String>(),
-            )
+            overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
@@ -530,10 +509,7 @@
                         activationState = ActivationState.NotSupported,
                     )
                 )
-            assertThat(endConfig())
-                .isEqualTo(
-                    KeyguardQuickAffordanceModel.Hidden,
-                )
+            assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
             assertThat(underTest.getSelections())
                 .isEqualTo(
                     mapOf(
@@ -543,7 +519,7 @@
                                     id = homeControls.key,
                                     name = homeControls.pickerName(),
                                     iconResourceId = homeControls.pickerIconResourceId,
-                                ),
+                                )
                             ),
                         KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
                     )
@@ -551,7 +527,7 @@
 
             underTest.select(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                quickAccessWallet.key
+                quickAccessWallet.key,
             )
 
             assertThat(startConfig())
@@ -564,10 +540,7 @@
                         activationState = ActivationState.NotSupported,
                     )
                 )
-            assertThat(endConfig())
-                .isEqualTo(
-                    KeyguardQuickAffordanceModel.Hidden,
-                )
+            assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
             assertThat(underTest.getSelections())
                 .isEqualTo(
                     mapOf(
@@ -577,7 +550,7 @@
                                     id = quickAccessWallet.key,
                                     name = quickAccessWallet.pickerName(),
                                     iconResourceId = quickAccessWallet.pickerIconResourceId,
-                                ),
+                                )
                             ),
                         KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
                     )
@@ -614,7 +587,7 @@
                                     id = quickAccessWallet.key,
                                     name = quickAccessWallet.pickerName(),
                                     iconResourceId = quickAccessWallet.pickerIconResourceId,
-                                ),
+                                )
                             ),
                         KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
                             listOf(
@@ -622,7 +595,7 @@
                                     id = qrCodeScanner.key,
                                     name = qrCodeScanner.pickerName(),
                                     iconResourceId = qrCodeScanner.pickerIconResourceId,
-                                ),
+                                )
                             ),
                     )
                 )
@@ -653,10 +626,7 @@
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
             underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
 
-            assertThat(startConfig())
-                .isEqualTo(
-                    KeyguardQuickAffordanceModel.Hidden,
-                )
+            assertThat(startConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
             assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
@@ -677,24 +647,18 @@
                                     id = quickAccessWallet.key,
                                     name = quickAccessWallet.pickerName(),
                                     iconResourceId = quickAccessWallet.pickerIconResourceId,
-                                ),
+                                )
                             ),
                     )
                 )
 
             underTest.unselect(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                quickAccessWallet.key
+                quickAccessWallet.key,
             )
 
-            assertThat(startConfig())
-                .isEqualTo(
-                    KeyguardQuickAffordanceModel.Hidden,
-                )
-            assertThat(endConfig())
-                .isEqualTo(
-                    KeyguardQuickAffordanceModel.Hidden,
-                )
+            assertThat(startConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+            assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
             assertThat(underTest.getSelections())
                 .isEqualTo(
                     mapOf<String, List<String>>(
@@ -768,15 +732,12 @@
                                     id = quickAccessWallet.key,
                                     name = quickAccessWallet.pickerName(),
                                     iconResourceId = quickAccessWallet.pickerIconResourceId,
-                                ),
+                                )
                             ),
                     )
                 )
 
-            underTest.unselect(
-                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                null,
-            )
+            underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, null)
 
             assertThat(underTest.getSelections())
                 .isEqualTo(
@@ -787,15 +748,29 @@
                 )
         }
 
+    @EnableSceneContainer
+    @Test
+    fun updatesLaunchingAffordanceFromCameraLauncher() =
+        testScope.runTest {
+            val launchingAffordance by collectLastValue(underTest.launchingAffordance)
+            runCurrent()
+
+            kosmos.cameraLauncher.setLaunchingAffordance(true)
+            runCurrent()
+            assertThat(launchingAffordance).isTrue()
+
+            kosmos.cameraLauncher.setLaunchingAffordance(false)
+            runCurrent()
+            assertThat(launchingAffordance).isFalse()
+        }
+
     companion object {
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
         private val ICON: Icon =
             Icon.Resource(
                 res = CONTENT_DESCRIPTION_RESOURCE_ID,
                 contentDescription =
-                    ContentDescription.Resource(
-                        res = CONTENT_DESCRIPTION_RESOURCE_ID,
-                    ),
+                    ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID),
             )
     }
 }
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 12eadfc..b5e670c 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
@@ -36,6 +36,7 @@
 import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -82,6 +83,7 @@
     private val communalRepository by lazy { kosmos.communalSceneRepository }
     private val screenOffAnimationController by lazy { kosmos.screenOffAnimationController }
     private val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository }
+    private val pulseExpansionInteractor by lazy { kosmos.pulseExpansionInteractor }
     private val notificationsKeyguardInteractor by lazy { kosmos.notificationsKeyguardInteractor }
     private val dozeParameters by lazy { kosmos.dozeParameters }
     private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@@ -188,7 +190,7 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            notificationsKeyguardInteractor.setPulseExpanding(true)
+            pulseExpansionInteractor.setPulseExpanding(true)
             deviceEntryRepository.setBypassEnabled(false)
             runCurrent()
 
@@ -200,7 +202,7 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(true)
             notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
             runCurrent()
@@ -219,7 +221,7 @@
                 to = KeyguardState.DOZING,
                 testScope,
             )
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(false)
             notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
@@ -239,7 +241,7 @@
                 to = KeyguardState.DOZING,
                 testScope,
             )
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(true)
@@ -260,7 +262,7 @@
                 to = KeyguardState.DOZING,
                 testScope,
             )
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
@@ -276,7 +278,7 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(true)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
@@ -298,7 +300,7 @@
         testScope.runTest {
             val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
             runCurrent()
-            notificationsKeyguardInteractor.setPulseExpanding(false)
+            pulseExpansionInteractor.setPulseExpanding(false)
             deviceEntryRepository.setBypassEnabled(false)
             whenever(dozeParameters.alwaysOn).thenReturn(true)
             whenever(dozeParameters.displayNeedsBlanking).thenReturn(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt
index 2e2894d..b5fc52f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt
@@ -24,12 +24,16 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl
+import com.android.systemui.media.controls.domain.pipeline.mediaDataManager
+import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
 import com.android.systemui.testKosmos
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestResult
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.test.setMain
 import org.junit.After
@@ -39,7 +43,7 @@
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() {
-    protected val kosmos = testKosmos()
+    protected val kosmos = testKosmos().apply { mediaDataManager = legacyMediaDataManagerImpl }
 
     protected val lifecycleOwner =
         TestLifecycleOwner(
@@ -62,11 +66,15 @@
     }
 
     protected inline fun TestScope.testWithinLifecycle(
-        crossinline block: suspend TestScope.() -> TestResult
+        usingMedia: Boolean = true,
+        crossinline block: suspend TestScope.() -> TestResult,
     ): TestResult {
         return runTest {
+            kosmos.usingMediaInComposeFragment = usingMedia
+
             lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
             underTest.activateIn(kosmos.testScope)
+            runCurrent()
             block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 3b00f86..9fe9ed2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -25,6 +25,15 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.controller.mediaCarouselController
+import com.android.systemui.media.controls.ui.view.MediaHostState
+import com.android.systemui.media.controls.ui.view.qqsMediaHost
+import com.android.systemui.media.controls.ui.view.qsMediaHost
+import com.android.systemui.qs.composefragment.viewmodel.MediaState.ACTIVE_MEDIA
+import com.android.systemui.qs.composefragment.viewmodel.MediaState.ANY_MEDIA
+import com.android.systemui.qs.composefragment.viewmodel.MediaState.NO_MEDIA
 import com.android.systemui.qs.fgsManagerController
 import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
 import com.android.systemui.res.R
@@ -35,9 +44,11 @@
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -185,6 +196,92 @@
             }
         }
 
+    @Test
+    fun qqsMediaHost_initializedCorrectly() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                assertThat(underTest.qqsMediaHost.location)
+                    .isEqualTo(MediaHierarchyManager.LOCATION_QQS)
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+                assertThat(underTest.qqsMediaHost.showsOnlyActiveMedia).isTrue()
+                assertThat(underTest.qqsMediaHost.hostView).isNotNull()
+            }
+        }
+
+    @Test
+    fun qsMediaHost_initializedCorrectly() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                assertThat(underTest.qsMediaHost.location)
+                    .isEqualTo(MediaHierarchyManager.LOCATION_QS)
+                assertThat(underTest.qsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+                assertThat(underTest.qsMediaHost.showsOnlyActiveMedia).isFalse()
+                assertThat(underTest.qsMediaHost.hostView).isNotNull()
+            }
+        }
+
+    @Test
+    fun qqsMediaVisible_onlyWhenActiveMedia() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false)
+
+                assertThat(underTest.qqsMediaVisible).isEqualTo(underTest.qqsMediaHost.visible)
+
+                setMediaState(NO_MEDIA)
+                assertThat(underTest.qqsMediaVisible).isFalse()
+
+                setMediaState(ANY_MEDIA)
+                assertThat(underTest.qqsMediaVisible).isFalse()
+
+                setMediaState(ACTIVE_MEDIA)
+                assertThat(underTest.qqsMediaVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun qsMediaVisible_onAnyMedia() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false)
+
+                assertThat(underTest.qsMediaVisible).isEqualTo(underTest.qsMediaHost.visible)
+
+                setMediaState(NO_MEDIA)
+                assertThat(underTest.qsMediaVisible).isFalse()
+
+                setMediaState(ANY_MEDIA)
+                assertThat(underTest.qsMediaVisible).isTrue()
+
+                setMediaState(ACTIVE_MEDIA)
+                assertThat(underTest.qsMediaVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun notUsingMedia_mediaNotVisible() =
+        with(kosmos) {
+            testScope.testWithinLifecycle(usingMedia = false) {
+                setMediaState(ACTIVE_MEDIA)
+
+                assertThat(underTest.qqsMediaVisible).isFalse()
+                assertThat(underTest.qsMediaVisible).isFalse()
+            }
+        }
+
+    private fun TestScope.setMediaState(state: MediaState) {
+        with(kosmos) {
+            val activeMedia = state == ACTIVE_MEDIA
+            val anyMedia = state != NO_MEDIA
+            whenever(legacyMediaDataManagerImpl.hasActiveMediaOrRecommendation())
+                .thenReturn(activeMedia)
+            whenever(legacyMediaDataManagerImpl.hasAnyMediaOrRecommendation()).thenReturn(anyMedia)
+            qqsMediaHost.updateViewVisibility()
+            qsMediaHost.updateViewVisibility()
+        }
+        runCurrent()
+    }
+
     companion object {
         private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS
 
@@ -195,3 +292,9 @@
         private const val epsilon = 0.001f
     }
 }
+
+private enum class MediaState {
+    ACTIVE_MEDIA,
+    ANY_MEDIA,
+    NO_MEDIA,
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
index 2c894f9..ab5a049 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt
@@ -20,21 +20,25 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class QuickQuickSettingsViewModelTest : SysuiTestCase() {
@@ -65,7 +69,8 @@
             fakeConfigurationRepository.onConfigurationChange()
         }
 
-    private val underTest = kosmos.quickQuickSettingsViewModel
+    private val underTest =
+        kosmos.quickQuickSettingsViewModelFactory.create().apply { activateIn(kosmos.testScope) }
 
     @Before
     fun setUp() {
@@ -77,17 +82,15 @@
         with(kosmos) {
             testScope.runTest {
                 setRows(2)
-                val columns by collectLastValue(underTest.columns)
-                val tileViewModels by collectLastValue(underTest.tileViewModels)
 
-                assertThat(columns).isEqualTo(4)
+                assertThat(underTest.columns).isEqualTo(4)
                 // All tiles in 4 columns
                 // [1] [2] [3 3]
                 // [4] [5 5]
                 // [6 6] [7] [8]
                 // [9 9]
 
-                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(5))
+                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(5))
             }
         }
 
@@ -96,10 +99,8 @@
         with(kosmos) {
             testScope.runTest {
                 setRows(2)
-                val columns by collectLastValue(underTest.columns)
-                val tileViewModels by collectLastValue(underTest.tileViewModels)
 
-                assertThat(columns).isEqualTo(4)
+                assertThat(underTest.columns).isEqualTo(4)
                 // All tiles in 4 columns
                 // [1] [2] [3 3]
                 // [4] [5 5]
@@ -107,9 +108,9 @@
                 // [9 9]
 
                 setRows(3)
-                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(8))
+                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(8))
                 setRows(1)
-                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(3))
+                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(3))
             }
         }
 
@@ -118,10 +119,8 @@
         with(kosmos) {
             testScope.runTest {
                 setRows(2)
-                val columns by collectLastValue(underTest.columns)
-                val tileViewModels by collectLastValue(underTest.tileViewModels)
 
-                assertThat(columns).isEqualTo(4)
+                assertThat(underTest.columns).isEqualTo(4)
                 // All tiles in 4 columns
                 // [1] [2] [3 3]
                 // [4] [5 5]
@@ -130,8 +129,9 @@
 
                 // Remove tile small:4
                 currentTilesInteractor.removeTiles(setOf(tiles[3]))
+                runCurrent()
 
-                assertThat(tileViewModels!!.map { it.tile.spec })
+                assertThat(underTest.tileViewModels.map { it.tile.spec })
                     .isEqualTo(
                         listOf(
                                 "$PREFIX_SMALL:1",
@@ -149,12 +149,15 @@
         currentTilesInteractor.setTiles(tiles)
     }
 
-    private fun Kosmos.setRows(rows: Int) {
-        testCase.context.orCreateTestableResources.addOverride(
-            R.integer.quick_qs_paginated_grid_num_rows,
-            rows,
-        )
-        fakeConfigurationRepository.onConfigurationChange()
+    private fun TestScope.setRows(rows: Int) {
+        with(kosmos) {
+            testCase.context.orCreateTestableResources.addOverride(
+                R.integer.quick_qs_paginated_grid_num_rows,
+                rows,
+            )
+            fakeConfigurationRepository.onConfigurationChange()
+        }
+        runCurrent()
     }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 4995920..2c8f7cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -32,6 +32,7 @@
 import com.android.internal.logging.uiEventLoggerFake
 import com.android.internal.policy.IKeyguardDismissCallback
 import com.android.keyguard.AuthInteractionProperties
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
@@ -71,8 +72,6 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor
-import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
-import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
@@ -128,6 +127,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -159,7 +159,8 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
+        whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+            .thenReturn(true)
         underTest = kosmos.sceneContainerStartable
     }
 
@@ -405,10 +406,7 @@
             assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer)
             underTest.start()
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-
+            updateFingerprintAuthStatus(isSuccess = true)
             assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
@@ -430,10 +428,7 @@
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             underTest.start()
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-
+            updateFingerprintAuthStatus(isSuccess = true)
             assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
             assertThat(alternateBouncerVisible).isFalse()
         }
@@ -464,9 +459,7 @@
             runCurrent()
             assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            updateFingerprintAuthStatus(isSuccess = true)
             runCurrent()
 
             assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
@@ -501,10 +494,7 @@
             assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer)
             assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Lockscreen)
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-
+            updateFingerprintAuthStatus(isSuccess = true)
             assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
             assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Gone)
         }
@@ -535,10 +525,7 @@
             runCurrent()
             assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer)
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-
+            updateFingerprintAuthStatus(isSuccess = true)
             assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
@@ -554,10 +541,7 @@
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             underTest.start()
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-
+            updateFingerprintAuthStatus(isSuccess = true)
             assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
@@ -592,9 +576,7 @@
             transitionStateFlowValue.value = ObservableTransitionState.Idle(Scenes.Shade)
             assertThat(currentSceneKey).isEqualTo(Scenes.Shade)
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            updateFingerprintAuthStatus(isSuccess = true)
             runCurrent()
 
             assertThat(currentSceneKey).isEqualTo(Scenes.Shade)
@@ -786,6 +768,7 @@
     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playSuccessHaptics_onSuccessfulLockscreenAuth_udfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -795,24 +778,19 @@
             assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()
 
             underTest.start()
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNotNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper)
-                .vibrateAuthSuccess(
-                    "SceneContainerStartable, $currentSceneKey device-entry::success"
-                )
+            verify(vibratorHelper).vibrateAuthSuccess(anyString())
             verify(vibratorHelper, never()).vibrateAuthError(anyString())
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_udfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -822,21 +800,19 @@
             assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()
 
             underTest.start()
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNotNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
             assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playSuccessHaptics_onSuccessfulLockscreenAuth_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -847,24 +823,19 @@
 
             underTest.start()
             allowHapticsOnSfps()
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNotNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper)
-                .vibrateAuthSuccess(
-                    "SceneContainerStartable, $currentSceneKey device-entry::success"
-                )
+            verify(vibratorHelper).vibrateAuthSuccess(anyString())
             verify(vibratorHelper, never()).vibrateAuthError(anyString())
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -875,15 +846,12 @@
 
             underTest.start()
             allowHapticsOnSfps()
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNotNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
             assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
@@ -902,8 +870,7 @@
 
             assertThat(playErrorHaptic).isNotNull()
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper)
-                .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error")
+            verify(vibratorHelper).vibrateAuthError(anyString())
             verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
         }
 
@@ -943,8 +910,7 @@
 
             assertThat(playErrorHaptic).isNotNull()
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper)
-                .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error")
+            verify(vibratorHelper).vibrateAuthError(anyString())
             verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
         }
 
@@ -972,6 +938,7 @@
     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun skipsSuccessHaptics_whenPowerButtonDown_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -982,24 +949,19 @@
 
             underTest.start()
             allowHapticsOnSfps(isPowerButtonDown = true)
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper, never())
-                .vibrateAuthSuccess(
-                    "SceneContainerStartable, $currentSceneKey device-entry::success"
-                )
+            verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
             verify(vibratorHelper, never()).vibrateAuthError(anyString())
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun skipsMSDLSuccessHaptics_whenPowerButtonDown_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -1010,21 +972,19 @@
 
             underTest.start()
             allowHapticsOnSfps(isPowerButtonDown = true)
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isNull()
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun skipsSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -1035,24 +995,19 @@
 
             underTest.start()
             allowHapticsOnSfps(lastPowerPress = 50)
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper, never())
-                .vibrateAuthSuccess(
-                    "SceneContainerStartable, $currentSceneKey device-entry::success"
-                )
+            verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
             verify(vibratorHelper, never()).vibrateAuthError(anyString())
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun skipsMSDLSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() =
         testScope.runTest {
+            whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
             val playSuccessHaptic by
                 collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
@@ -1063,15 +1018,12 @@
 
             underTest.start()
             allowHapticsOnSfps(lastPowerPress = 50)
-            unlockWithFingerprintAuth()
+            // unlock with fingerprint
+            updateFingerprintAuthStatus(isSuccess = true)
 
             assertThat(playSuccessHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isNull()
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
-
-            updateFingerprintAuthStatus(isSuccess = true)
-            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
         }
 
     @Test
@@ -1090,9 +1042,7 @@
             updateFingerprintAuthStatus(isSuccess = false)
 
             assertThat(playErrorHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper, never())
-                .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error")
+            verify(vibratorHelper, never()).vibrateAuthError(anyString())
             verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
         }
 
@@ -1112,7 +1062,6 @@
             updateFingerprintAuthStatus(isSuccess = false)
 
             assertThat(playErrorHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isNull()
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
         }
@@ -1132,9 +1081,7 @@
             updateFaceAuthStatus(isSuccess = false)
 
             assertThat(playErrorHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
-            verify(vibratorHelper, never())
-                .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error")
+            verify(vibratorHelper, never()).vibrateAuthError(anyString())
             verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
         }
 
@@ -1153,7 +1100,6 @@
             updateFaceAuthStatus(isSuccess = false)
 
             assertThat(playErrorHaptic).isNull()
-            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             assertThat(msdlPlayer.latestTokenPlayed).isNull()
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
         }
@@ -1176,9 +1122,7 @@
                 )
                 .forEachIndexed { index, sceneKey ->
                     if (sceneKey == Scenes.Gone) {
-                        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                            SuccessFingerprintAuthenticationStatus(0, true)
-                        )
+                        updateFingerprintAuthStatus(isSuccess = true)
                         runCurrent()
                     }
                     fakeSceneDataSource.pause()
@@ -1334,9 +1278,7 @@
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
             underTest.start()
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            updateFingerprintAuthStatus(isSuccess = true)
             runCurrent()
             powerInteractor.setAwakeForTest()
             runCurrent()
@@ -1586,9 +1528,7 @@
             runCurrent()
             verify(falsingCollector).onBouncerShown()
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            updateFingerprintAuthStatus(isSuccess = true)
             runCurrent()
             sceneInteractor.changeScene(Scenes.Gone, "reason")
             runCurrent()
@@ -2350,9 +2290,7 @@
             val dismissCallback: IKeyguardDismissCallback = mock()
             kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
 
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            updateFingerprintAuthStatus(isSuccess = true)
             runCurrent()
             kosmos.fakeExecutor.runAllReady()
 
@@ -2641,13 +2579,6 @@
         runCurrent()
     }
 
-    private fun unlockWithFingerprintAuth() {
-        kosmos.fakeKeyguardRepository.setBiometricUnlockSource(
-            BiometricUnlockSource.FINGERPRINT_SENSOR
-        )
-        kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.UNLOCK_COLLAPSING)
-    }
-
     private fun TestScope.setupBiometricAuth(
         hasSfps: Boolean = false,
         hasUdfps: Boolean = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index ea2de25..01c17bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -619,7 +619,8 @@
                         mScreenOffAnimationController,
                         new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()),
                         notifsKeyguardInteractor,
-                        mKosmos.getCommunalInteractor());
+                        mKosmos.getCommunalInteractor(),
+                        mKosmos.getPulseExpansionInteractor());
         mConfigurationController = new ConfigurationControllerImpl(mContext);
         PulseExpansionHandler expansionHandler = new PulseExpansionHandler(
                 mContext,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 9f752a8..3b5d358 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
@@ -86,6 +87,7 @@
 
     private var bypassEnabled: Boolean = false
     private var statusBarState: Int = StatusBarState.KEYGUARD
+
     private fun eased(dozeAmount: Float) =
         notificationWakeUpCoordinator.dozeAmountInterpolator.getInterpolation(dozeAmount)
 
@@ -119,6 +121,7 @@
                 logger,
                 kosmos.notificationsKeyguardInteractor,
                 kosmos.communalInteractor,
+                kosmos.pulseExpansionInteractor,
             )
         statusBarStateCallback = withArgCaptor {
             verify(statusBarStateController).addCallback(capture())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
index 133a114..dcc8ecd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
@@ -58,19 +58,4 @@
 
             assertThat(notifsFullyHidden).isTrue()
         }
-
-    @Test
-    fun isPulseExpanding_reflectsRepository() =
-        testComponent.runTest {
-            underTest.setPulseExpanding(false)
-            val isPulseExpanding by collectLastValue(underTest.isPulseExpanding)
-            runCurrent()
-
-            assertThat(isPulseExpanding).isFalse()
-
-            underTest.setPulseExpanding(true)
-            runCurrent()
-
-            assertThat(isPulseExpanding).isTrue()
-        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 2349c25..07935e4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.stack
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -30,12 +32,14 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 private const val MAX_PULSE_HEIGHT = 100000f
 
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
-class AmbientStateTest : SysuiTestCase() {
+class AmbientStateTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     private val dumpManager = mock<DumpManager>()
     private val sectionProvider = StackScrollAlgorithm.SectionProvider { _, _ -> false }
@@ -46,6 +50,18 @@
 
     private lateinit var sut: AmbientState
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setUp() {
         sut =
@@ -56,7 +72,7 @@
                 bypassController,
                 statusBarKeyguardViewManager,
                 largeScreenShadeInterpolator,
-                avalancheController
+                avalancheController,
             )
     }
 
@@ -97,6 +113,7 @@
 
         assertThat(sut.pulseHeight).isEqualTo(expected)
     }
+
     // endregion
 
     // region statusBarState
@@ -119,6 +136,7 @@
 
         assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue()
     }
+
     // endregion
 
     // region hideAmount
@@ -141,6 +159,7 @@
 
         assertThat(sut.pulseHeight).isEqualTo(1f)
     }
+
     // endregion
 
     // region dozeAmount
@@ -173,6 +192,7 @@
 
         assertThat(sut.pulseHeight).isEqualTo(1f)
     }
+
     // endregion
 
     // region trackedHeadsUpRow
@@ -189,10 +209,12 @@
 
         assertThat(sut.trackedHeadsUpRow).isNull()
     }
+
     // endregion
 
     // region isSwipingUp
     @Test
+    @DisableSceneContainer
     fun isSwipingUp_whenValueChangedToTrue_shouldRequireFling() {
         sut.isSwipingUp = false
         sut.isFlingRequiredAfterLockScreenSwipeUp = false
@@ -203,6 +225,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun isSwipingUp_whenValueChangedToFalse_shouldRequireFling() {
         sut.isSwipingUp = true
         sut.isFlingRequiredAfterLockScreenSwipeUp = false
@@ -211,10 +234,12 @@
 
         assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue()
     }
+
     // endregion
 
     // region isFlinging
     @Test
+    @DisableSceneContainer
     fun isFlinging_shouldNotNeedFling() {
         sut.arrangeFlinging(true)
 
@@ -224,6 +249,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun isFlinging_whenNotOnLockScreen_shouldDoNothing() {
         sut.arrangeFlinging(true)
         sut.setStatusBarState(StatusBarState.SHADE)
@@ -235,6 +261,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun isFlinging_whenValueChangedToTrue_shouldDoNothing() {
         sut.arrangeFlinging(false)
 
@@ -242,10 +269,12 @@
 
         assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue()
     }
+
     // endregion
 
     // region scrollY
     @Test
+    @DisableSceneContainer
     fun scrollY_shouldSetValueGreaterThanZero() {
         sut.scrollY = 0
 
@@ -255,6 +284,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun scrollY_shouldNotSetValueLessThanZero() {
         sut.scrollY = 0
 
@@ -262,21 +292,24 @@
 
         assertThat(sut.scrollY).isEqualTo(0)
     }
+
     // endregion
 
     // region setOverScrollAmount
+    @Test
+    @DisableSceneContainer
     fun setOverScrollAmount_shouldSetValueOnTop() {
-        sut.setOverScrollAmount(/* amount = */ 10f, /* onTop = */ true)
+        sut.setOverScrollAmount(/* amount= */ 10f, /* onTop= */ true)
 
-        val resultOnTop = sut.getOverScrollAmount(/* top = */ true)
-        val resultOnBottom = sut.getOverScrollAmount(/* top = */ false)
+        val resultOnTop = sut.getOverScrollAmount(/* top= */ true)
+        val resultOnBottom = sut.getOverScrollAmount(/* top= */ false)
 
         assertThat(resultOnTop).isEqualTo(10f)
         assertThat(resultOnBottom).isEqualTo(0f)
     }
 
     fun setOverScrollAmount_shouldSetValueOnBottom() {
-        sut.setOverScrollAmount(/* amount = */ 10f, /* onTop = */ false)
+        sut.setOverScrollAmount(/* amount= */ 10f, /* onTop= */ false)
 
         val resultOnTop = sut.getOverScrollAmount(/* top */ true)
         val resultOnBottom = sut.getOverScrollAmount(/* top */ false)
@@ -284,6 +317,7 @@
         assertThat(resultOnTop).isEqualTo(0f)
         assertThat(resultOnBottom).isEqualTo(10f)
     }
+
     // endregion
 
     // region IsPulseExpanding
@@ -317,6 +351,7 @@
 
         assertThat(sut.isPulseExpanding).isFalse()
     }
+
     // endregion
 
     // region isOnKeyguard
@@ -333,6 +368,7 @@
 
         assertThat(sut.isOnKeyguard).isFalse()
     }
+
     // endregion
 
     // region mIsClosing
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index d5a7c89..a940ed4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,6 +19,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
@@ -66,10 +67,13 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.mockLargeScreenHeaderHelper
 import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition
 import com.android.systemui.testKosmos
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
@@ -165,6 +169,83 @@
         }
 
     @Test
+    fun validateHorizontalPositionSingleShade() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(false)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
+        }
+
+    @Test
+    fun validateHorizontalPositionSplitShade() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(true)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition)
+            assertThat(horizontalPosition.ratio).isEqualTo(0.5f)
+        }
+
+    @Test
+    @EnableSceneContainer
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun validateHorizontalPositionInSceneContainerSingleShade() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(false)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
+        }
+
+    @Test
+    @EnableSceneContainer
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun validateHorizontalPositionInSceneContainerSplitShade() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(true)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition)
+            assertThat(horizontalPosition.ratio).isEqualTo(0.5f)
+        }
+
+    @Test
+    @EnableSceneContainer
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun validateHorizontalPositionInDualShade_narrowLayout() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(false)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
+        }
+
+    @Test
+    @EnableSceneContainer
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun validateHorizontalPositionInDualShade_wideLayout() =
+        testScope.runTest {
+            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+            shadeTestUtil.setSplitShade(true)
+
+            val horizontalPosition = checkNotNull(dimens).horizontalPosition
+            assertIs<HorizontalPosition.FloatAtEnd>(horizontalPosition)
+            assertThat(horizontalPosition.width).isEqualTo(200)
+        }
+
+    @Test
     fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() =
         testScope.runTest {
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt
index 48c2cc7..a008588 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt
@@ -20,6 +20,8 @@
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
@@ -113,4 +115,21 @@
 
             assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
         }
+
+    @Test
+    @DisableSceneContainer
+    fun entireScreenTouchable_communalVisible() =
+        testScope.runTest {
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+
+            kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal)
+            runCurrent()
+
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()
+
+            kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Blank)
+            runCurrent()
+
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt
new file mode 100644
index 0000000..c90183d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.statusbar.phone.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.configureKeyguardBypass
+import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shadeTestUtil
+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)
+@EnableSceneContainer
+class KeyguardBypassInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private lateinit var underTest: KeyguardBypassInteractor
+
+    @Test
+    fun canBypassFalseWhenBypassAvailableFalse() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipIsBypassAvailableCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isFalse()
+        }
+
+    @Test
+    fun canBypassTrueOnPrimaryBouncerShowing() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipBouncerShowingCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isTrue()
+        }
+
+    @Test
+    fun canBypassTrueOnAlternateBouncerShowing() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipAlternateBouncerShowingCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isTrue()
+        }
+
+    @Test
+    fun canBypassFalseWhenNotOnLockscreenScene() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipOnLockscreenSceneCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            runCurrent()
+            assertThat(currentScene).isNotEqualTo(Scenes.Lockscreen)
+            assertThat(canBypass).isFalse()
+        }
+
+    @Test
+    fun canBypassFalseOnLaunchingAffordance() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipLaunchingAffordanceCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isFalse()
+        }
+
+    @Test
+    fun canBypassFalseOnPulseExpanding() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipPulseExpandingCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isFalse()
+        }
+
+    @Test
+    fun canBypassFalseOnQsExpanded() =
+        testScope.runTest {
+            initializeDependenciesForCanBypass(skipQsExpandedCheck = false)
+            val canBypass by collectLastValue(underTest.canBypass)
+            runCurrent()
+            assertThat(canBypass).isFalse()
+        }
+
+    // Initializes all canBypass dependencies to opposite of value needed to return
+    private fun initializeDependenciesForCanBypass(
+        skipIsBypassAvailableCheck: Boolean = true,
+        skipBouncerShowingCheck: Boolean = true,
+        skipAlternateBouncerShowingCheck: Boolean = true,
+        skipOnLockscreenSceneCheck: Boolean = true,
+        skipLaunchingAffordanceCheck: Boolean = true,
+        skipPulseExpandingCheck: Boolean = true,
+        skipQsExpandedCheck: Boolean = true,
+    ) {
+        // !isBypassAvailable false
+        kosmos.configureKeyguardBypass(isBypassAvailable = skipIsBypassAvailableCheck)
+        underTest = kosmos.keyguardBypassInteractor
+
+        // bouncerShowing false, !onLockscreenScene false
+        // !onLockscreenScene false
+        setScene(
+            bouncerShowing = !skipBouncerShowingCheck,
+            onLockscreenScene = skipOnLockscreenSceneCheck,
+        )
+        // alternateBouncerShowing false
+        setAlternateBouncerShowing(!skipAlternateBouncerShowingCheck)
+        // launchingAffordance false
+        setLaunchingAffordance(!skipLaunchingAffordanceCheck)
+        // pulseExpanding false
+        setPulseExpanding(!skipPulseExpandingCheck)
+        // qsExpanding false
+        setQsExpanded(!skipQsExpandedCheck)
+    }
+
+    private fun setAlternateBouncerShowing(alternateBouncerVisible: Boolean) {
+        kosmos.keyguardBouncerRepository.setAlternateVisible(alternateBouncerVisible)
+    }
+
+    private fun setScene(bouncerShowing: Boolean, onLockscreenScene: Boolean) {
+        if (bouncerShowing) {
+            kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "reason")
+        } else if (onLockscreenScene) {
+            kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+        } else {
+            kosmos.sceneInteractor.changeScene(Scenes.Shade, "reason")
+        }
+    }
+
+    private fun setLaunchingAffordance(launchingAffordance: Boolean) {
+        kosmos.keyguardQuickAffordanceInteractor.setLaunchingAffordance(launchingAffordance)
+    }
+
+    private fun setPulseExpanding(pulseExpanding: Boolean) {
+        kosmos.pulseExpansionInteractor.setPulseExpanding(pulseExpanding)
+    }
+
+    private fun setQsExpanded(qsExpanded: Boolean) {
+        if (qsExpanded) {
+            kosmos.shadeTestUtil.setQsExpansion(1f)
+        } else {
+            kosmos.shadeTestUtil.setQsExpansion(0f)
+        }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index e264264..5792175 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -203,12 +203,12 @@
     fun onZenDataChanged(data: ZenData)
 
     /** Update reactive axes for this clock */
-    fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>)
+    fun onFontAxesChanged(axes: List<ClockFontAxisSetting>)
 }
 
 /** Axis setting value for a clock */
-data class ClockReactiveSetting(
-    /** Axis key; matches ClockReactiveAxis.key */
+data class ClockFontAxisSetting(
+    /** Axis key; matches ClockFontAxis.key */
     val key: String,
 
     /** Value to set this axis to */
@@ -323,11 +323,11 @@
     val isReactiveToTone: Boolean = true,
 
     /** Font axes that can be modified on this clock */
-    val axes: List<ClockReactiveAxis> = listOf(),
+    val axes: List<ClockFontAxis> = listOf(),
 )
 
 /** Represents an Axis that can be modified */
-data class ClockReactiveAxis(
+data class ClockFontAxis(
     /** Axis key, not user renderable */
     val key: String,
 
@@ -348,15 +348,17 @@
 
     /** Description of the axis */
     val description: String,
-)
+) {
+    fun toSetting() = ClockFontAxisSetting(key, currentValue)
+}
 
 /** Axis user interaction modes */
 enum class AxisType {
-    /** Boolean toggle. Swaps between minValue & maxValue */
-    Toggle,
+    /** Continuous range between minValue & maxValue. */
+    Float,
 
-    /** Continuous slider between minValue & maxValue */
-    Slider,
+    /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */
+    Boolean,
 }
 
 /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
@@ -404,7 +406,7 @@
 data class ClockSettings(
     val clockId: ClockId? = null,
     val seedColor: Int? = null,
-    val axes: List<ClockReactiveSetting>? = null,
+    val axes: List<ClockFontAxisSetting>? = null,
 ) {
     // Exclude metadata from equality checks
     var metadata: JSONObject = JSONObject()
diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
index ca62d28..f38f42d 100644
--- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
@@ -23,4 +23,7 @@
     <dimen name="keyguard_clock_top_margin">80dp</dimen>
     <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">155dp</dimen>
     <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">85dp</dimen>
+
+    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
+    <dimen name="shade_panel_width">474dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw800dp/dimens.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml
index 0d82217..8ae3a2e 100644
--- a/packages/SystemUI/res/values-sw800dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw800dp/dimens.xml
@@ -21,4 +21,7 @@
 
     <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
     <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+
+    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
+    <dimen name="shade_panel_width">392dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-w1000dp/dimens.xml b/packages/SystemUI/res/values-w1000dp/dimens.xml
new file mode 100644
index 0000000..b3f7acd
--- /dev/null
+++ b/packages/SystemUI/res/values-w1000dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<resources>
+    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
+    <dimen name="shade_panel_width">474dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index f96a0b9..d4a52c3 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -338,6 +338,9 @@
     <!-- Whether to show the full screen user switcher. -->
     <bool name="config_enableFullscreenUserSwitcher">false</bool>
 
+    <!-- Whether to go to the launcher when unlocking via an occluding app -->
+    <bool name="config_goToHomeFromOccludedApps">false</bool>
+
     <!-- Determines whether the shell features all run on another thread. -->
     <bool name="config_enableShellMainThread">true</bool>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c2d942f..de547ad 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -577,6 +577,13 @@
 
     <dimen name="notification_panel_margin_horizontal">0dp</dimen>
 
+    <!--
+    The width of the shade overlay panel (both notifications and quick settings). On Compact screens
+    in portrait orientation (< w600dp) this is ignored, and the shade layout takes up the full
+    screen width.
+    -->
+    <dimen name="shade_panel_width">412dp</dimen>
+
     <dimen name="brightness_mirror_height">48dp</dimen>
 
     <dimen name="volume_dialog_panel_transparent_padding_right">8dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9602163..ab64ae5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3930,10 +3930,10 @@
     </string>
     <!-- Title for the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
     <string name="qs_edit_mode_reset_dialog_title">
-        Reset tiles
+        Reset all tiles?
     </string>
     <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
     <string name="qs_edit_mode_reset_dialog_content">
-        Reset tiles to their original order and sizes?
+        All Quick Settings tiles will reset to the device’s original settings
     </string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 69ab976..12b5fc0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -33,7 +33,6 @@
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.Flags;
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -323,7 +322,7 @@
         final boolean isLandscape = mContext.getResources().getConfiguration().orientation
                 == Configuration.ORIENTATION_LANDSCAPE;
         mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
-        mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mEffectiveUserId,
+        mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mConfig.mUserId,
                 getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName,
                 false /*onSwitchToCredential*/, isLandscape);
 
@@ -676,10 +675,8 @@
 
         final Runnable endActionRunnable = () -> {
             setVisibility(View.INVISIBLE);
-            if (Flags.customBiometricPrompt()) {
                 // TODO(b/288175645): resetPrompt calls should be lifecycle aware
                 mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
-            }
             removeWindowIfAttached();
         };
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
index 4997370..b070068 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -3,6 +3,7 @@
 import android.app.admin.DevicePolicyManager
 import android.app.admin.DevicePolicyResources
 import android.content.Context
+import android.hardware.biometrics.Flags
 import android.os.UserManager
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockscreenCredential
@@ -71,13 +72,22 @@
         // Request LockSettingsService to return the Gatekeeper Password in the
         // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
         // Gatekeeper Password and operationId.
-        val effectiveUserId = request.userInfo.deviceCredentialOwnerId
+        var effectiveUserId = request.userInfo.userIdForPasswordEntry
         val response =
-            lockPatternUtils.verifyCredential(
-                credential,
-                effectiveUserId,
-                LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE
-            )
+            if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) {
+                effectiveUserId = request.userInfo.userId
+                lockPatternUtils.verifyTiedProfileChallenge(
+                    credential,
+                    request.userInfo.userId,
+                    LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
+                )
+            } else {
+                lockPatternUtils.verifyCredential(
+                    credential,
+                    effectiveUserId,
+                    LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
+                )
+            }
 
         if (response.isMatched) {
             lockPatternUtils.userPresent(effectiveUserId)
@@ -91,7 +101,7 @@
                 lockPatternUtils.verifyGatekeeperPasswordHandle(
                     pwHandle,
                     request.operationInfo.gatekeeperChallenge,
-                    effectiveUserId
+                    effectiveUserId,
                 )
             val hat = gkResponse.gatekeeperHAT
             lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
@@ -108,7 +118,7 @@
                     CredentialStatus.Fail.Throttled(
                         applicationContext.getString(
                             R.string.biometric_dialog_credential_too_many_attempts,
-                            remaining / 1000
+                            remaining / 1000,
                         )
                     )
                 )
@@ -129,10 +139,10 @@
                         applicationContext.getString(
                             R.string.biometric_dialog_credential_attempts_before_wipe,
                             numAttempts,
-                            maxAttempts
+                            maxAttempts,
                         ),
                         remainingAttempts,
-                        fetchFinalAttemptMessageOrNull(request, remainingAttempts)
+                        fetchFinalAttemptMessageOrNull(request, remainingAttempts),
                     )
                 )
             }
@@ -150,9 +160,9 @@
                 devicePolicyManager,
                 userManager.getUserTypeForWipe(
                     devicePolicyManager,
-                    request.userInfo.deviceCredentialOwnerId
+                    request.userInfo.deviceCredentialOwnerId,
                 ),
-                remainingAttempts
+                remainingAttempts,
             )
         } else {
             null
@@ -205,7 +215,7 @@
     }
 
 private fun Context.getLastAttemptBeforeWipeDeviceMessage(
-    request: BiometricPromptRequest.Credential,
+    request: BiometricPromptRequest.Credential
 ): String {
     val id =
         when (request) {
@@ -249,7 +259,7 @@
 }
 
 private fun Context.getLastAttemptBeforeWipeUserMessage(
-    request: BiometricPromptRequest.Credential,
+    request: BiometricPromptRequest.Credential
 ): String {
     val resId =
         when (request) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index 6da5e42..008fb26 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.biometrics.domain.interactor
 
-import android.hardware.biometrics.Flags
 import android.hardware.biometrics.PromptInfo
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.biometrics.Utils
@@ -104,6 +103,7 @@
 constructor(
     fingerprintPropertyRepository: FingerprintPropertyRepository,
     private val displayStateInteractor: DisplayStateInteractor,
+    private val credentialInteractor: CredentialInteractor,
     private val promptRepository: PromptRepository,
     private val lockPatternUtils: LockPatternUtils,
 ) : PromptSelectorInteractor {
@@ -177,7 +177,7 @@
 
     override fun setPrompt(
         promptInfo: PromptInfo,
-        effectiveUserId: Int,
+        userId: Int,
         requestId: Long,
         modalities: BiometricModalities,
         challenge: Long,
@@ -185,10 +185,10 @@
         onSwitchToCredential: Boolean,
         isLandscape: Boolean,
     ) {
+        val effectiveUserId = credentialInteractor.getCredentialOwnerOrSelfId(userId)
         val hasCredentialViewShown = promptKind.value.isCredential()
         val showBpForCredential =
-            Flags.customBiometricPrompt() &&
-                !Utils.isBiometricAllowed(promptInfo) &&
+            !Utils.isBiometricAllowed(promptInfo) &&
                 isDeviceCredentialAllowed(promptInfo) &&
                 promptInfo.contentView != null &&
                 !promptInfo.isContentViewMoreOptionsButtonUsed
@@ -211,10 +211,7 @@
                                 PromptKind.Biometric.PaneType.ONE_PANE_NO_SENSOR_LANDSCAPE
                             else -> PromptKind.Biometric.PaneType.TWO_PANE_LANDSCAPE
                         }
-                    PromptKind.Biometric(
-                        modalities,
-                        paneType = paneType,
-                    )
+                    PromptKind.Biometric(modalities, paneType = paneType)
                 } else {
                     PromptKind.Biometric(modalities)
                 }
@@ -224,7 +221,7 @@
 
         promptRepository.setPrompt(
             promptInfo = promptInfo,
-            userId = effectiveUserId,
+            userId = userId,
             requestId = requestId,
             gatekeeperChallenge = challenge,
             kind = kind,
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 2df5f16..db4b0f2 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
@@ -22,7 +22,6 @@
 import android.hardware.biometrics.BiometricAuthenticator
 import android.hardware.biometrics.BiometricConstants
 import android.hardware.biometrics.BiometricPrompt
-import android.hardware.biometrics.Flags
 import android.hardware.face.FaceManager
 import android.util.Log
 import android.view.MotionEvent
@@ -93,7 +92,7 @@
         val attributes =
             view.context.obtainStyledAttributes(
                 R.style.TextAppearance_AuthCredential_Indicator,
-                intArrayOf(android.R.attr.textColor)
+                intArrayOf(android.R.attr.textColor),
             )
         val textColorHint = attributes.getColor(0, 0)
         attributes.recycle()
@@ -130,13 +129,13 @@
             object : AccessibilityDelegateCompat() {
                 override fun onInitializeAccessibilityNodeInfo(
                     host: View,
-                    info: AccessibilityNodeInfoCompat
+                    info: AccessibilityNodeInfoCompat,
                 ) {
                     super.onInitializeAccessibilityNodeInfo(host, info)
                     info.addAction(
                         AccessibilityActionCompat(
                             AccessibilityNodeInfoCompat.ACTION_CLICK,
-                            view.context.getString(R.string.biometric_dialog_cancel_authentication)
+                            view.context.getString(R.string.biometric_dialog_cancel_authentication),
                         )
                     )
                 }
@@ -189,13 +188,11 @@
             subtitleView.text = viewModel.subtitle.first()
             descriptionView.text = viewModel.description.first()
 
-            if (Flags.customBiometricPrompt()) {
-                BiometricCustomizedViewBinder.bind(
-                    customizedViewContainer,
-                    viewModel.contentView.first(),
-                    legacyCallback
-                )
-            }
+            BiometricCustomizedViewBinder.bind(
+                customizedViewContainer,
+                viewModel.contentView.first(),
+                legacyCallback,
+            )
 
             // set button listeners
             negativeButton.setOnClickListener { legacyCallback.onButtonNegative() }
@@ -233,10 +230,7 @@
             lifecycleScope.launch {
                 viewModel.hideSensorIcon.collect { showWithoutIcon ->
                     if (!showWithoutIcon) {
-                        PromptIconViewBinder.bind(
-                            iconView,
-                            viewModel,
-                        )
+                        PromptIconViewBinder.bind(iconView, viewModel)
                     }
                 }
             }
@@ -421,7 +415,7 @@
                     launch {
                         viewModel.onAnnounceAccessibilityHint(
                             event,
-                            accessibilityManager.isTouchExplorationEnabled
+                            accessibilityManager.isTouchExplorationEnabled,
                         )
                     }
                     false
@@ -444,10 +438,7 @@
                                         haptics.flag,
                                     )
                                 } else {
-                                    vibratorHelper.performHapticFeedback(
-                                        view,
-                                        haptics.constant,
-                                    )
+                                    vibratorHelper.performHapticFeedback(view, haptics.constant)
                                 }
                             }
                             is PromptViewModel.HapticsToPlay.MSDL -> {
@@ -561,14 +552,12 @@
             viewModel.showAuthenticated(
                 modality = authenticatedModality,
                 dismissAfterDelay = 500,
-                helpMessage = if (msgId != null) applicationContext.getString(msgId) else ""
+                helpMessage = if (msgId != null) applicationContext.getString(msgId) else "",
             )
         }
     }
 
-    private fun getHelpForSuccessfulAuthentication(
-        authenticatedModality: BiometricModality,
-    ): Int? {
+    private fun getHelpForSuccessfulAuthentication(authenticatedModality: BiometricModality): Int? {
         // for coex, show a message when face succeeds after fingerprint has also started
         if (authenticatedModality != BiometricModality.Face) {
             return null
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
index 3ea91f0..39543e7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -1,6 +1,5 @@
 package com.android.systemui.biometrics.ui.binder
 
-import android.hardware.biometrics.Flags
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
@@ -67,7 +66,7 @@
                     updateForContentDimensions(
                         containerWidth,
                         containerHeight,
-                        0 // animateDurationMs
+                        0, // animateDurationMs
                     )
                 }
             }
@@ -81,13 +80,11 @@
 
                         subtitleView.textOrHide = header.subtitle
                         descriptionView.textOrHide = header.description
-                        if (Flags.customBiometricPrompt()) {
-                            BiometricCustomizedViewBinder.bind(
-                                customizedViewContainer,
-                                header.contentView,
-                                legacyCallback
-                            )
-                        }
+                        BiometricCustomizedViewBinder.bind(
+                            customizedViewContainer,
+                            header.contentView,
+                            legacyCallback,
+                        )
 
                         iconView?.setImageDrawable(header.icon)
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index 761c3da..0c5c723 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -2,7 +2,6 @@
 
 import android.content.Context
 import android.graphics.drawable.Drawable
-import android.hardware.biometrics.Flags.customBiometricPrompt
 import android.hardware.biometrics.PromptContentView
 import android.text.InputType
 import com.android.internal.widget.LockPatternView
@@ -36,21 +35,17 @@
     val header: Flow<CredentialHeaderViewModel> =
         combine(
             credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(),
-            credentialInteractor.showTitleOnly
+            credentialInteractor.showTitleOnly,
         ) { request, showTitleOnly ->
-            val flagEnabled = customBiometricPrompt()
-            val showTitleOnlyForCredential = showTitleOnly && flagEnabled
             BiometricPromptHeaderViewModelImpl(
                 request,
                 user = request.userInfo,
                 title = request.title,
-                subtitle = if (showTitleOnlyForCredential) "" else request.subtitle,
-                contentView =
-                    if (flagEnabled && !showTitleOnlyForCredential) request.contentView else null,
-                description =
-                    if (flagEnabled && request.contentView != null) "" else request.description,
+                subtitle = if (showTitleOnly) "" else request.subtitle,
+                contentView = if (!showTitleOnly) request.contentView else null,
+                description = if (request.contentView != null) "" else request.description,
                 icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId),
-                showEmergencyCallButton = request.showEmergencyCallButton
+                showEmergencyCallButton = request.showEmergencyCallButton,
             )
         }
 
@@ -125,7 +120,7 @@
     /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
     suspend fun checkCredential(
         pattern: List<LockPatternView.Cell>,
-        header: CredentialHeaderViewModel
+        header: CredentialHeaderViewModel,
     ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
 
     private suspend fun checkCredential(result: CredentialStatus) {
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 0ac9405..cbf783d 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
@@ -27,7 +27,6 @@
 import android.graphics.drawable.Drawable
 import android.hardware.biometrics.BiometricFingerprintConstants
 import android.hardware.biometrics.BiometricPrompt
-import android.hardware.biometrics.Flags.customBiometricPrompt
 import android.hardware.biometrics.PromptContentView
 import android.os.UserHandle
 import android.text.TextPaint
@@ -145,13 +144,13 @@
                     rotatedBounds,
                     params.naturalDisplayWidth,
                     params.naturalDisplayHeight,
-                    rotation.ordinal
+                    rotation.ordinal,
                 )
                 Rect(
                     rotatedBounds.left,
                     rotatedBounds.top,
                     params.logicalDisplayWidth - rotatedBounds.right,
-                    params.logicalDisplayHeight - rotatedBounds.bottom
+                    params.logicalDisplayHeight - rotatedBounds.bottom,
                 )
             }
             .distinctUntilChanged()
@@ -263,7 +262,7 @@
                 promptKind,
                 displayStateInteractor.isLargeScreen,
                 displayStateInteractor.currentRotation,
-                modalities
+                modalities,
             ) { forceLarge, promptKind, isLargeScreen, rotation, modalities ->
                 when {
                     forceLarge ||
@@ -351,7 +350,7 @@
                                 0,
                                 0,
                                 landscapeMediumHorizontalPadding,
-                                landscapeMediumBottomPadding
+                                landscapeMediumBottomPadding,
                             )
                         }
                     PromptPosition.Left ->
@@ -365,7 +364,7 @@
                                 landscapeMediumHorizontalPadding,
                                 0,
                                 0,
-                                landscapeMediumBottomPadding
+                                landscapeMediumBottomPadding,
                             )
                         }
                     PromptPosition.Top ->
@@ -474,7 +473,7 @@
         promptSelectorInteractor.prompt
             .map {
                 when {
-                    !(customBiometricPrompt()) || it == null -> Pair(null, "")
+                    it == null -> Pair(null, "")
                     else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager)
                 }
             }
@@ -490,9 +489,7 @@
 
     /** Custom content view for the prompt. */
     val contentView: Flow<PromptContentView?> =
-        promptSelectorInteractor.prompt
-            .map { if (customBiometricPrompt()) it?.contentView else null }
-            .distinctUntilChanged()
+        promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged()
 
     private val originalDescription =
         promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
@@ -521,7 +518,7 @@
                 val attributes =
                     context.obtainStyledAttributes(
                         R.style.TextAppearance_AuthCredential_Title,
-                        intArrayOf(android.R.attr.textSize)
+                        intArrayOf(android.R.attr.textSize),
                     )
                 val paint = TextPaint()
                 paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat()
@@ -566,7 +563,7 @@
     private fun getHorizontalPadding(
         size: PromptSize,
         modalities: BiometricModalities,
-        hasOnlyOneLineTitle: Boolean
+        hasOnlyOneLineTitle: Boolean,
     ) =
         if (size.isSmall) {
             -smallHorizontalGuidelinePadding
@@ -582,21 +579,13 @@
 
     /** If the indicator (help, error) message should be shown. */
     val isIndicatorMessageVisible: Flow<Boolean> =
-        combine(
-            size,
-            position,
-            message,
-        ) { size, _, message ->
+        combine(size, position, message) { size, _, message ->
             size.isMedium && message.message.isNotBlank()
         }
 
     /** If the auth is pending confirmation and the confirm button should be shown. */
     val isConfirmButtonVisible: Flow<Boolean> =
-        combine(
-            size,
-            position,
-            isPendingConfirmation,
-        ) { size, _, isPendingConfirmation ->
+        combine(size, position, isPendingConfirmation) { size, _, isPendingConfirmation ->
             size.isNotSmall && isPendingConfirmation
         }
 
@@ -606,24 +595,22 @@
 
     /** If the negative button should be shown. */
     val isNegativeButtonVisible: Flow<Boolean> =
-        combine(
+        combine(size, position, isAuthenticated, promptSelectorInteractor.isCredentialAllowed) {
             size,
-            position,
-            isAuthenticated,
-            promptSelectorInteractor.isCredentialAllowed,
-        ) { size, _, authState, credentialAllowed ->
+            _,
+            authState,
+            credentialAllowed ->
             size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
         }
 
     /** If the cancel button should be shown (. */
     val isCancelButtonVisible: Flow<Boolean> =
-        combine(
+        combine(size, position, isAuthenticated, isNegativeButtonVisible, isConfirmButtonVisible) {
             size,
-            position,
-            isAuthenticated,
-            isNegativeButtonVisible,
-            isConfirmButtonVisible,
-        ) { size, _, authState, showNegativeButton, showConfirmButton ->
+            _,
+            authState,
+            showNegativeButton,
+            showConfirmButton ->
             size.isNotSmall && authState.isAuthenticated && !showNegativeButton && showConfirmButton
         }
 
@@ -633,33 +620,28 @@
      * fingerprint sensor.
      */
     val canTryAgainNow: Flow<Boolean> =
-        combine(
-            _canTryAgainNow,
+        combine(_canTryAgainNow, size, position, isAuthenticated, isRetrySupported) {
+            readyToTryAgain,
             size,
-            position,
-            isAuthenticated,
-            isRetrySupported,
-        ) { readyToTryAgain, size, _, authState, supportsRetry ->
+            _,
+            authState,
+            supportsRetry ->
             readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated
         }
 
     /** If the try again button show be shown (only the button, see [canTryAgainNow]). */
     val isTryAgainButtonVisible: Flow<Boolean> =
-        combine(
-            canTryAgainNow,
-            modalities,
-        ) { tryAgainIsPossible, modalities ->
+        combine(canTryAgainNow, modalities) { tryAgainIsPossible, modalities ->
             tryAgainIsPossible && modalities.hasFaceOnly
         }
 
     /** If the credential fallback button show be shown. */
     val isCredentialButtonVisible: Flow<Boolean> =
-        combine(
+        combine(size, position, isAuthenticated, promptSelectorInteractor.isCredentialAllowed) {
             size,
-            position,
-            isAuthenticated,
-            promptSelectorInteractor.isCredentialAllowed,
-        ) { size, _, authState, credentialAllowed ->
+            _,
+            authState,
+            credentialAllowed ->
             size.isMedium && authState.isNotAuthenticated && credentialAllowed
         }
 
@@ -759,10 +741,7 @@
      *
      * Ignored if the user has already authenticated.
      */
-    suspend fun showTemporaryHelp(
-        message: String,
-        messageAfterHelp: String = "",
-    ) = coroutineScope {
+    suspend fun showTemporaryHelp(message: String, messageAfterHelp: String = "") = coroutineScope {
         if (_isAuthenticated.value.isAuthenticated) {
             return@coroutineScope
         }
@@ -910,13 +889,13 @@
                 udfpsUtils.getTouchInNativeCoordinates(
                     event.getPointerId(0),
                     event,
-                    udfpsOverlayParams.value
+                    udfpsOverlayParams.value,
                 )
             if (
                 !udfpsUtils.isWithinSensorArea(
                     event.getPointerId(0),
                     event,
-                    udfpsOverlayParams.value
+                    udfpsOverlayParams.value,
                 )
             ) {
                 _accessibilityHint.emit(
@@ -925,7 +904,7 @@
                         context,
                         scaledTouch.x,
                         scaledTouch.y,
-                        udfpsOverlayParams.value
+                        udfpsOverlayParams.value,
                     )
                 )
             }
@@ -948,10 +927,7 @@
             if (msdlFeedback()) {
                 HapticsToPlay.MSDL(MSDLToken.UNLOCK, authInteractionProperties)
             } else {
-                HapticsToPlay.HapticConstant(
-                    HapticFeedbackConstants.BIOMETRIC_CONFIRM,
-                    flag = null,
-                )
+                HapticsToPlay.HapticConstant(HapticFeedbackConstants.BIOMETRIC_CONFIRM, flag = null)
             }
         _hapticsToPlay.value = haptics
     }
@@ -961,10 +937,7 @@
             if (msdlFeedback()) {
                 HapticsToPlay.MSDL(MSDLToken.FAILURE, authInteractionProperties)
             } else {
-                HapticsToPlay.HapticConstant(
-                    HapticFeedbackConstants.BIOMETRIC_REJECT,
-                    flag = null,
-                )
+                HapticsToPlay.HapticConstant(HapticFeedbackConstants.BIOMETRIC_REJECT, flag = null)
             }
         _hapticsToPlay.value = haptics
     }
@@ -1006,7 +979,7 @@
 private fun Context.getUserBadgedLogoInfo(
     prompt: BiometricPromptRequest.Biometric,
     iconProvider: IconProvider,
-    activityTaskManager: ActivityTaskManager
+    activityTaskManager: ActivityTaskManager,
 ): Pair<Drawable?, String> {
     var icon: Drawable? =
         if (prompt.logoBitmap != null) BitmapDrawable(resources, prompt.logoBitmap) else null
@@ -1045,7 +1018,7 @@
                 packageManager
                     .getUserBadgedLabel(
                         packageManager.getApplicationLabel(appInfo),
-                        UserHandle.of(userId)
+                        UserHandle.of(userId),
                     )
                     .toString()
         }
@@ -1070,7 +1043,7 @@
 
 private fun BiometricPromptRequest.Biometric.getApplicationInfo(
     context: Context,
-    componentNameForLogo: ComponentName?
+    componentNameForLogo: ComponentName?,
 ): ApplicationInfo? {
     val packageName =
         when {
@@ -1088,7 +1061,7 @@
         try {
             context.packageManager.getApplicationInfo(
                 packageName,
-                PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
+                PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER,
             )
         } catch (e: PackageManager.NameNotFoundException) {
             Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName", e)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
index 572794d..cf80b7d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt
@@ -36,7 +36,7 @@
         /** Converts from span to communal content size. */
         fun toSize(span: Int): CommunalContentSize {
             return entries.find { it.span == span }
-                ?: throw Exception("Invalid span for communal content size")
+                ?: throw IllegalArgumentException("$span is not a valid span size")
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 6fb6236..4cdf286 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
 import com.android.systemui.dreams.DreamMonitor
-import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
+import com.android.systemui.dreams.homecontrols.system.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.haptics.msdl.MSDLCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 59c8f06..cb649f2 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -70,6 +70,9 @@
 import com.android.systemui.inputmethod.InputMethodModule;
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
 import com.android.systemui.keyguard.ui.composable.LockscreenContent;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.log.dagger.MonitorLog;
@@ -227,6 +230,7 @@
         InputMethodModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
+        KeyguardDataQuickAffordanceModule.class,
         LetterboxModule.class,
         LogModule.class,
         MediaProjectionActivitiesModule.class,
@@ -428,6 +432,11 @@
                 sysuiUiBgExecutor));
     }
 
+    @Provides
+    static KeyguardQuickAffordancesMetricsLogger providesKeyguardQuickAffordancesMetricsLogger() {
+        return new KeyguardQuickAffordancesMetricsLoggerImpl();
+    }
+
     @Binds
     abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl);
 
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
index 782bce4..41a59a9 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
@@ -19,10 +19,12 @@
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.util.kotlin.FlowDumperImpl
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
@@ -46,16 +48,17 @@
 class DeviceEntryHapticsInteractor
 @Inject
 constructor(
-    deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
-    deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
-    deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
-    fingerprintPropertyRepository: FingerprintPropertyRepository,
     biometricSettingsRepository: BiometricSettingsRepository,
+    deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
+    deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
+    deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+    fingerprintPropertyRepository: FingerprintPropertyRepository,
     keyEventInteractor: KeyEventInteractor,
+    private val logger: BiometricUnlockLogger,
     powerInteractor: PowerInteractor,
     private val systemClock: SystemClock,
-    private val logger: BiometricUnlockLogger,
-) {
+    dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
     private val powerButtonSideFpsEnrolled =
         combineTransform(
                 fingerprintPropertyRepository.sensorType,
@@ -86,7 +89,7 @@
                     powerButtonSideFpsEnrolled,
                     powerButtonDown,
                     lastPowerButtonWakeup,
-                    ::Triple
+                    ::Triple,
                 )
             )
             .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
@@ -100,14 +103,19 @@
                 }
                 allowHaptic
             }
-            .map {} // map to Unit
+            // map to Unit
+            .map {}
+            .dumpWhileCollecting("playSuccessHaptic")
 
     private val playErrorHapticForBiometricFailure: Flow<Unit> =
         merge(
                 deviceEntryFingerprintAuthInteractor.fingerprintFailure,
                 deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure,
             )
-            .map {} // map to Unit
+            // map to Unit
+            .map {}
+            .dumpWhileCollecting("playErrorHapticForBiometricFailure")
+
     val playErrorHaptic: Flow<Unit> =
         playErrorHapticForBiometricFailure
             .sample(combine(powerButtonSideFpsEnrolled, powerButtonDown, ::Pair))
@@ -118,7 +126,9 @@
                 }
                 allowHaptic
             }
-            .map {} // map to Unit
+            // map to Unit
+            .map {}
+            .dumpWhileCollecting("playErrorHaptic")
 
     private val recentPowerButtonPressThresholdMs = 400L
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt
index 0b9336f..b2d4405 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt
@@ -16,18 +16,49 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import androidx.annotation.VisibleForTesting
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_DISMISS_BOUNCER
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_ONLY_WAKE
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_SHOW_BOUNCER
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_UNLOCK_COLLAPSING
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
+import com.android.systemui.statusbar.phone.DozeScrimController
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
+import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 /**
  * Hosts application business logic related to the source of the user entering the device. Note: The
@@ -42,13 +73,184 @@
 class DeviceEntrySourceInteractor
 @Inject
 constructor(
+    authenticationInteractor: AuthenticationInteractor,
+    authController: AuthController,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
+    deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
+    dozeScrimController: DozeScrimController,
+    keyguardBypassInteractor: KeyguardBypassInteractor,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
     keyguardInteractor: KeyguardInteractor,
-) {
+    sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor,
+    sceneInteractor: SceneInteractor,
+    dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+    private val isShowingBouncerScene: Flow<Boolean> =
+        sceneInteractor.transitionState
+            .map { transitionState ->
+                transitionState.isIdle(Scenes.Bouncer) ||
+                    transitionState.isTransitioning(null, Scenes.Bouncer)
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("isShowingBouncerScene")
+
+    private val isUnlockedWithStrongFaceUnlock =
+        deviceEntryFaceAuthInteractor.authenticationStatus
+            .map { status ->
+                (status as? SuccessFaceAuthenticationStatus)?.successResult?.isStrongBiometric
+                    ?: false
+            }
+            .dumpWhileCollecting("unlockedWithStrongFaceUnlock")
+
+    private val isUnlockedWithStrongFingerprintUnlock =
+        deviceEntryFingerprintAuthInteractor.authenticationStatus
+            .map { status ->
+                (status as? SuccessFingerprintAuthenticationStatus)?.isStrongBiometric ?: false
+            }
+            .dumpWhileCollecting("unlockedWithStrongFingerprintUnlock")
+
+    private val faceWakeAndUnlockMode: Flow<BiometricUnlockMode> =
+        combine(
+                alternateBouncerInteractor.isVisible,
+                keyguardBypassInteractor.isBypassAvailable,
+                isUnlockedWithStrongFaceUnlock,
+                sceneContainerOcclusionInteractor.isOccludingActivityShown,
+                sceneInteractor.currentScene,
+                isShowingBouncerScene,
+            ) {
+                isAlternateBouncerVisible,
+                isBypassAvailable,
+                isFaceStrongBiometric,
+                isOccluded,
+                currentScene,
+                isShowingBouncerScene ->
+                val isUnlockingAllowed =
+                    keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(isFaceStrongBiometric)
+                val bypass = isBypassAvailable || authController.isUdfpsFingerDown()
+
+                when {
+                    !keyguardUpdateMonitor.isDeviceInteractive ->
+                        when {
+                            !isUnlockingAllowed -> if (bypass) MODE_SHOW_BOUNCER else MODE_NONE
+                            bypass -> MODE_WAKE_AND_UNLOCK_PULSING
+                            else -> MODE_ONLY_WAKE
+                        }
+
+                    isUnlockingAllowed && currentScene == Scenes.Dream ->
+                        if (bypass) MODE_WAKE_AND_UNLOCK_FROM_DREAM else MODE_ONLY_WAKE
+
+                    isUnlockingAllowed && isOccluded -> MODE_UNLOCK_COLLAPSING
+
+                    (isShowingBouncerScene || isAlternateBouncerVisible) && isUnlockingAllowed ->
+                        MODE_DISMISS_BOUNCER
+
+                    isUnlockingAllowed && bypass -> MODE_UNLOCK_COLLAPSING
+
+                    bypass -> MODE_SHOW_BOUNCER
+
+                    else -> MODE_NONE
+                }
+            }
+            .map { biometricModeIntToObject(it) }
+            .dumpWhileCollecting("faceWakeAndUnlockMode")
+
+    private val fingerprintWakeAndUnlockMode: Flow<BiometricUnlockMode> =
+        combine(
+                alternateBouncerInteractor.isVisible,
+                authenticationInteractor.authenticationMethod,
+                sceneInteractor.currentScene,
+                isUnlockedWithStrongFingerprintUnlock,
+                isShowingBouncerScene,
+            ) {
+                alternateBouncerVisible,
+                authenticationMethod,
+                currentScene,
+                isFingerprintStrongBiometric,
+                isShowingBouncerScene ->
+                val unlockingAllowed =
+                    keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                        isFingerprintStrongBiometric
+                    )
+                when {
+                    !keyguardUpdateMonitor.isDeviceInteractive ->
+                        when {
+                            dozeScrimController.isPulsing && unlockingAllowed ->
+                                MODE_WAKE_AND_UNLOCK_PULSING
+
+                            unlockingAllowed || !authenticationMethod.isSecure ->
+                                MODE_WAKE_AND_UNLOCK
+
+                            else -> MODE_SHOW_BOUNCER
+                        }
+
+                    unlockingAllowed && currentScene == Scenes.Dream ->
+                        MODE_WAKE_AND_UNLOCK_FROM_DREAM
+
+                    isShowingBouncerScene && unlockingAllowed -> MODE_DISMISS_BOUNCER
+
+                    unlockingAllowed -> MODE_UNLOCK_COLLAPSING
+
+                    currentScene != Scenes.Bouncer && !alternateBouncerVisible -> MODE_SHOW_BOUNCER
+
+                    else -> MODE_NONE
+                }
+            }
+            .map { biometricModeIntToObject(it) }
+            .dumpWhileCollecting("fingerprintWakeAndUnlockMode")
+
+    @VisibleForTesting
+    private val biometricUnlockStateOnKeyguardDismissed =
+        merge(
+                fingerprintWakeAndUnlockMode
+                    .filter { BiometricUnlockMode.dismissesKeyguard(it) }
+                    .map { mode ->
+                        BiometricUnlockModel(mode, BiometricUnlockSource.FINGERPRINT_SENSOR)
+                    },
+                faceWakeAndUnlockMode
+                    .filter { BiometricUnlockMode.dismissesKeyguard(it) }
+                    .map { mode -> BiometricUnlockModel(mode, BiometricUnlockSource.FACE_SENSOR) },
+            )
+            .dumpWhileCollecting("biometricUnlockState")
+
+    private val deviceEntryFingerprintAuthWakeAndUnlockEvents:
+        Flow<SuccessFingerprintAuthenticationStatus> =
+        deviceEntryFingerprintAuthInteractor.authenticationStatus
+            .filterIsInstance<SuccessFingerprintAuthenticationStatus>()
+            .dumpWhileCollecting("deviceEntryFingerprintAuthSuccessEvents")
+
+    private val deviceEntryFaceAuthWakeAndUnlockEvents: Flow<SuccessFaceAuthenticationStatus> =
+        deviceEntryFaceAuthInteractor.authenticationStatus
+            .filterIsInstance<SuccessFaceAuthenticationStatus>()
+            .sampleFilter(
+                combine(
+                    sceneContainerOcclusionInteractor.isOccludingActivityShown,
+                    keyguardBypassInteractor.isBypassAvailable,
+                    keyguardBypassInteractor.canBypass,
+                    ::Triple,
+                )
+            ) { (isOccludingActivityShown, isBypassAvailable, canBypass) ->
+                isOccludingActivityShown || !isBypassAvailable || canBypass
+            }
+            .dumpWhileCollecting("deviceEntryFaceAuthSuccessEvents")
+
+    private val deviceEntryBiometricAuthSuccessEvents =
+        merge(deviceEntryFingerprintAuthWakeAndUnlockEvents, deviceEntryFaceAuthWakeAndUnlockEvents)
+            .dumpWhileCollecting("deviceEntryBiometricAuthSuccessEvents")
+
     val deviceEntryFromBiometricSource: Flow<BiometricUnlockSource> =
-        keyguardInteractor.biometricUnlockState
-            .filter { BiometricUnlockMode.dismissesKeyguard(it.mode) }
-            .map { it.source }
-            .filterNotNull()
+        if (SceneContainerFlag.isEnabled) {
+                deviceEntryBiometricAuthSuccessEvents
+                    .sample(biometricUnlockStateOnKeyguardDismissed)
+                    .map { it.source }
+                    .filterNotNull()
+            } else {
+                keyguardInteractor.biometricUnlockState
+                    .filter { BiometricUnlockMode.dismissesKeyguard(it.mode) }
+                    .map { it.source }
+                    .filterNotNull()
+            }
+            .dumpWhileCollecting("deviceEntryFromBiometricSource")
 
     private val attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = MutableSharedFlow()
     val deviceEntryFromDeviceEntryIcon: Flow<Unit> =
@@ -60,4 +262,18 @@
     suspend fun attemptEnterDeviceFromDeviceEntryIcon() {
         attemptEnterDeviceFromDeviceEntryIcon.emit(Unit)
     }
+
+    private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockMode {
+        return when (value) {
+            MODE_NONE -> BiometricUnlockMode.NONE
+            MODE_WAKE_AND_UNLOCK -> BiometricUnlockMode.WAKE_AND_UNLOCK
+            MODE_WAKE_AND_UNLOCK_PULSING -> BiometricUnlockMode.WAKE_AND_UNLOCK_PULSING
+            MODE_SHOW_BOUNCER -> BiometricUnlockMode.SHOW_BOUNCER
+            MODE_ONLY_WAKE -> BiometricUnlockMode.ONLY_WAKE
+            MODE_UNLOCK_COLLAPSING -> BiometricUnlockMode.UNLOCK_COLLAPSING
+            MODE_WAKE_AND_UNLOCK_FROM_DREAM -> BiometricUnlockMode.WAKE_AND_UNLOCK_FROM_DREAM
+            MODE_DISMISS_BOUNCER -> BiometricUnlockMode.DISMISS_BOUNCER
+            else -> throw IllegalArgumentException("Invalid BiometricUnlockModel value: $value")
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index a20556c..15631f8 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.Intent
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
@@ -34,6 +35,7 @@
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -48,7 +50,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Business logic for handling authentication events when an app is occluding the lockscreen. */
 @ExperimentalCoroutinesApi
@@ -123,19 +124,28 @@
             .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))
 
     init {
-        scope.launch {
-            // On fingerprint success when the screen is on and not dreaming, go to the home screen
-            fingerprintUnlockSuccessEvents
-                .sample(
-                    combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair)
-                )
-                .collect { (interactive, dreaming) ->
-                    if (interactive && !dreaming) {
-                        goToHomeScreen()
+        // This seems undesirable in most cases, except when a video is playing and can PiP when
+        // unlocked. It was originally added for tablets, so allow it there
+        if (context.resources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) {
+            scope.launch {
+                // On fingerprint success when the screen is on and not dreaming, go to the home
+                // screen
+                fingerprintUnlockSuccessEvents
+                    .sample(
+                        combine(
+                            powerInteractor.isInteractive,
+                            keyguardInteractor.isDreaming,
+                            ::Pair,
+                        )
+                    )
+                    .collect { (interactive, dreaming) ->
+                        if (interactive && !dreaming) {
+                            goToHomeScreen()
+                        }
+                        // don't go to the home screen if the authentication is from
+                        // AOD/dozing/off/dreaming
                     }
-                    // don't go to the home screen if the authentication is from
-                    // AOD/dozing/off/dreaming
-                }
+            }
         }
 
         scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index a45ad15..3171bbc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -33,9 +33,10 @@
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.SystemDialogsCloser;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
-import com.android.systemui.dreams.homecontrols.DreamServiceDelegate;
-import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl;
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsDataSourceModule;
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent;
+import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService;
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.pipeline.shared.TileSpec;
 import com.android.systemui.qs.shared.model.TileCategory;
@@ -61,13 +62,15 @@
  * Dagger Module providing Dream-related functionality.
  */
 @Module(includes = {
-            RegisteredComplicationsModule.class,
-            LowLightDreamModule.class,
-            ScrimModule.class
-        },
+        RegisteredComplicationsModule.class,
+        LowLightDreamModule.class,
+        ScrimModule.class,
+        HomeControlsDataSourceModule.class,
+},
         subcomponents = {
-            ComplicationComponent.class,
-            DreamOverlayComponent.class,
+                ComplicationComponent.class,
+                DreamOverlayComponent.class,
+                HomeControlsRemoteServiceComponent.class,
         })
 public interface DreamModule {
     String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
@@ -113,6 +116,15 @@
             HomeControlsDreamService service);
 
     /**
+     * Provides Home Controls Remote Service
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsRemoteService.class)
+    Service bindHomeControlsRemoteService(
+            HomeControlsRemoteService service);
+
+    /**
      * Provides a touch inset manager for dreams.
      */
     @Provides
@@ -202,10 +214,4 @@
                 QSTilePolicy.NoRestrictions.INSTANCE
                 );
     }
-
-
-    /** Provides delegate to allow for testing of dream service */
-    @Binds
-    DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl);
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt
deleted file mode 100644
index 2cfb02e..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.dreams.homecontrols
-
-import android.app.Activity
-import android.service.dreams.DreamService
-
-/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */
-interface DreamServiceDelegate {
-    /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */
-    fun getActivity(dreamService: DreamService): Activity?
-
-    /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */
-    fun wakeUp(dreamService: DreamService)
-
-    /** Wrapper for [DreamService.finish] which can be mocked in tests. */
-    fun finish(dreamService: DreamService)
-
-    /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */
-    fun redirectWake(dreamService: DreamService): Boolean
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt
deleted file mode 100644
index 7dc5434..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.dreams.homecontrols
-
-import android.app.Activity
-import android.service.dreams.DreamService
-import javax.inject.Inject
-
-class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate {
-    override fun getActivity(dreamService: DreamService): Activity {
-        return dreamService.activity
-    }
-
-    override fun finish(dreamService: DreamService) {
-        dreamService.finish()
-    }
-
-    override fun wakeUp(dreamService: DreamService) {
-        dreamService.wakeUp()
-    }
-
-    override fun redirectWake(dreamService: DreamService): Boolean {
-        return dreamService.redirectWake
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
index 6b14ebf..9eec1b6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -16,75 +16,109 @@
 
 package com.android.systemui.dreams.homecontrols
 
+import android.annotation.SuppressLint
 import android.content.Intent
 import android.os.PowerManager
 import android.service.controls.ControlsProviderService
 import android.service.dreams.DreamService
 import android.window.TaskFragmentInfo
-import com.android.systemui.controls.settings.ControlsSettingsRepository
-import com.android.systemui.coroutines.newTracingContext
-import com.android.systemui.dagger.qualifiers.Background
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ServiceLifecycleDispatcher
+import androidx.lifecycle.lifecycleScope
 import com.android.systemui.dreams.DreamLogger
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY
+import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.dagger.DreamLog
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.wakelock.WakeLock
-import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
 
+/**
+ * [DreamService] which embeds the user's chosen home controls app to allow it to display as a
+ * screensaver. This service will run in the foreground user context.
+ */
 class HomeControlsDreamService
 @Inject
-constructor(
-    private val controlsSettingsRepository: ControlsSettingsRepository,
-    private val taskFragmentFactory: TaskFragmentComponent.Factory,
-    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
-    private val wakeLockBuilder: WakeLock.Builder,
-    private val dreamServiceDelegate: DreamServiceDelegate,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @DreamLog logBuffer: LogBuffer
-) : DreamService() {
+constructor(private val factory: HomeControlsDreamServiceImpl.Factory) :
+    DreamService(), LifecycleOwner {
 
-    private val serviceJob = SupervisorJob()
-    private val serviceScope =
-        CoroutineScope(bgDispatcher + serviceJob + newTracingContext("HomeControlsDreamService"))
+    private val dispatcher = ServiceLifecycleDispatcher(this)
+    override val lifecycle: Lifecycle
+        get() = dispatcher.lifecycle
+
+    private val impl: HomeControlsDreamServiceImpl by lazy { factory.create(this, this) }
+
+    override fun onCreate() {
+        dispatcher.onServicePreSuperOnCreate()
+        super.onCreate()
+    }
+
+    override fun onDreamingStarted() {
+        dispatcher.onServicePreSuperOnStart()
+        super.onDreamingStarted()
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        impl.onAttachedToWindow()
+    }
+
+    override fun onDetachedFromWindow() {
+        dispatcher.onServicePreSuperOnDestroy()
+        super.onDetachedFromWindow()
+        impl.onDetachedFromWindow()
+    }
+}
+
+/**
+ * Implementation of the home controls dream service, which allows for injecting a [DreamService]
+ * and [LifecycleOwner] for testing.
+ */
+class HomeControlsDreamServiceImpl
+@AssistedInject
+constructor(
+    private val taskFragmentFactory: TaskFragmentComponent.Factory,
+    private val wakeLockBuilder: WakeLock.Builder,
+    private val powerManager: PowerManager,
+    private val systemClock: SystemClock,
+    private val dataSource: HomeControlsDataSource,
+    @DreamLog logBuffer: LogBuffer,
+    @Assisted private val service: DreamService,
+    @Assisted lifecycleOwner: LifecycleOwner,
+) : LifecycleOwner by lifecycleOwner {
+
     private val logger = DreamLogger(logBuffer, TAG)
     private lateinit var taskFragmentComponent: TaskFragmentComponent
     private val wakeLock: WakeLock by lazy {
         wakeLockBuilder
-            .setMaxTimeout(NO_TIMEOUT)
+            .setMaxTimeout(WakeLock.Builder.NO_TIMEOUT)
             .setTag(TAG)
             .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
             .build()
     }
 
-    override fun onAttachedToWindow() {
-        super.onAttachedToWindow()
-        val activity = dreamServiceDelegate.getActivity(this)
+    fun onAttachedToWindow() {
+        val activity = service.activity
         if (activity == null) {
-            finish()
+            service.finish()
             return
         }
-
-        // Start monitoring package updates to possibly restart the dream if the home controls
-        // package is updated while we are dreaming.
-        serviceScope.launch { homeControlsComponentInteractor.monitorUpdatesAndRestart() }
-
         taskFragmentComponent =
             taskFragmentFactory
                 .create(
                     activity = activity,
                     onCreateCallback = { launchActivity() },
                     onInfoChangedCallback = this::onTaskFragmentInfoChanged,
-                    hide = { endDream(false) }
+                    hide = { endDream(false) },
                 )
                 .apply { createTaskFragment() }
 
@@ -99,53 +133,61 @@
     }
 
     private fun endDream(handleRedirect: Boolean) {
-        homeControlsComponentInteractor.onDreamEndUnexpectedly()
-        if (handleRedirect && dreamServiceDelegate.redirectWake(this)) {
-            dreamServiceDelegate.wakeUp(this)
-            serviceScope.launch {
+        pokeUserActivity()
+        if (handleRedirect && service.redirectWake) {
+            service.wakeUp()
+            lifecycleScope.launch {
                 delay(ACTIVITY_RESTART_DELAY)
                 launchActivity()
             }
         } else {
-            dreamServiceDelegate.finish(this)
+            service.finish()
         }
     }
 
     private fun launchActivity() {
-        val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
-        val componentName = homeControlsComponentInteractor.panelComponent.value
-        logger.d("Starting embedding $componentName")
-        val intent =
-            Intent().apply {
-                component = componentName
-                putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
-                putExtra(
-                    ControlsProviderService.EXTRA_CONTROLS_SURFACE,
-                    ControlsProviderService.CONTROLS_SURFACE_DREAM
-                )
-            }
-        taskFragmentComponent.startActivityInTaskFragment(intent)
-    }
-
-    override fun onDetachedFromWindow() {
-        super.onDetachedFromWindow()
-        wakeLock.release(TAG)
-        taskFragmentComponent.destroy()
-        serviceScope.launch {
-            delay(CANCELLATION_DELAY_AFTER_DETACHED)
-            serviceJob.cancel("Dream detached from window")
+        lifecycleScope.launch {
+            val (componentName, setting) = dataSource.componentInfo.first()
+            logger.d("Starting embedding $componentName")
+            val intent =
+                Intent().apply {
+                    component = componentName
+                    putExtra(
+                        ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+                        setting,
+                    )
+                    putExtra(
+                        ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+                        ControlsProviderService.CONTROLS_SURFACE_DREAM,
+                    )
+                }
+            taskFragmentComponent.startActivityInTaskFragment(intent)
         }
     }
 
-    private companion object {
-        /**
-         * Defines how long after the dream ends that we should keep monitoring for package updates
-         * to attempt a restart of the dream. This should be larger than
-         * [MAX_UPDATE_CORRELATION_DELAY] as it also includes the time the package update takes to
-         * complete.
-         */
-        val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds
+    fun onDetachedFromWindow() {
+        wakeLock.release(TAG)
+        taskFragmentComponent.destroy()
+    }
 
+    @SuppressLint("MissingPermission")
+    private fun pokeUserActivity() {
+        powerManager.userActivity(
+            systemClock.uptimeMillis(),
+            PowerManager.USER_ACTIVITY_EVENT_OTHER,
+            PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS,
+        )
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            service: DreamService,
+            lifecycleOwner: LifecycleOwner,
+        ): HomeControlsDreamServiceImpl
+    }
+
+    companion object {
         /**
          * Defines the delay after wakeup where we should attempt to restart the embedded activity.
          * When a wakeup is redirected, the dream service may keep running. In this case, we should
@@ -153,6 +195,6 @@
          * after the wakeup transition has played.
          */
         val ACTIVITY_RESTART_DELAY = 334.milliseconds
-        const val TAG = "HomeControlsDreamService"
+        private const val TAG = "HomeControlsDreamServiceImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt
new file mode 100644
index 0000000..3a2791f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.dreams.homecontrols.dagger
+
+import com.android.systemui.Flags.homeControlsDreamHsum
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.homecontrols.service.RemoteHomeControlsDataSourceDelegator
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dreams.homecontrols.system.LocalHomeControlsDataSourceDelegator
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface HomeControlsDataSourceModule {
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun providesHomeControlsDataSource(
+            localSource: Lazy<LocalHomeControlsDataSourceDelegator>,
+            remoteSource: Lazy<RemoteHomeControlsDataSourceDelegator>,
+        ): HomeControlsDataSource {
+            return if (homeControlsDreamHsum()) {
+                remoteSource.get()
+            } else {
+                localSource.get()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt
new file mode 100644
index 0000000..500d15e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.dreams.homecontrols.dagger
+
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dreams.homecontrols.service.HomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.Observer
+import com.android.systemui.util.service.PersistentConnectionManager
+import com.android.systemui.util.service.dagger.ObservableServiceModule
+import dagger.BindsInstance
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import javax.inject.Named
+
+/**
+ * This component is responsible for generating the connection to the home controls remote service
+ * which runs in the SYSTEM_USER context and provides the data needed to run the home controls dream
+ * in the foreground user context.
+ */
+@Subcomponent(
+    modules =
+        [
+            ObservableServiceModule::class,
+            HomeControlsRemoteServiceComponent.HomeControlsRemoteServiceModule::class,
+        ]
+)
+interface HomeControlsRemoteServiceComponent {
+    /** Creates a [HomeControlsRemoteServiceComponent]. */
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(
+            @BindsInstance callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy>
+        ): HomeControlsRemoteServiceComponent
+    }
+
+    /** A [PersistentConnectionManager] pointing to the home controls remote service. */
+    val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy>
+
+    /** Scoped module providing specific components for the [ObservableServiceConnection]. */
+    @Module
+    interface HomeControlsRemoteServiceModule {
+        companion object {
+            @Provides
+            @Named(ObservableServiceModule.SERVICE_CONNECTION)
+            fun providesConnection(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>,
+                callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy>,
+            ): ObservableServiceConnection<HomeControlsRemoteProxy> {
+                connection.addCallback(callback)
+                return connection
+            }
+
+            /** Provides the wrapper around the home controls remote binder */
+            @Provides
+            fun providesTransformer(
+                factory: HomeControlsRemoteProxy.Factory
+            ): ObservableServiceConnection.ServiceTransformer<HomeControlsRemoteProxy> {
+                return ObservableServiceConnection.ServiceTransformer { service: IBinder ->
+                    factory.create(IHomeControlsRemoteProxy.Stub.asInterface(service))
+                }
+            }
+
+            /** Provides the intent to connect to [HomeControlsRemoteService] */
+            @Provides
+            fun providesIntent(@Application context: Context): Intent {
+                return Intent(context, HomeControlsRemoteService::class.java)
+            }
+
+            /** Provides no-op [Observer] since the remote service is in the same package */
+            @Provides
+            @Named(ObservableServiceModule.OBSERVER)
+            fun providesObserver(): Observer {
+                return object : Observer {
+                    override fun addCallback(callback: Observer.Callback?) {
+                        // no-op, do nothing
+                    }
+
+                    override fun removeCallback(callback: Observer.Callback?) {
+                        // no-op, do nothing
+                    }
+                }
+            }
+
+            /**
+             * Provides a name that will be used by [PersistentConnectionManager] when logging
+             * state.
+             */
+            @Provides
+            @Named(ObservableServiceModule.DUMPSYS_NAME)
+            fun providesDumpsysName(): String {
+                return "HomeControlsRemoteService"
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
deleted file mode 100644
index 2034138..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * 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.dreams.homecontrols.domain.interactor
-
-import android.annotation.SuppressLint
-import android.app.DreamManager
-import android.content.ComponentName
-import android.os.PowerManager
-import android.os.UserHandle
-import com.android.systemui.common.domain.interactor.PackageChangeInteractor
-import com.android.systemui.common.shared.model.PackageChangeModel
-import com.android.systemui.controls.ControlsServiceInfo
-import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.controls.panels.AuthorizedPanelsRepository
-import com.android.systemui.controls.panels.SelectedComponentRepository
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.getOrNull
-import com.android.systemui.util.kotlin.pairwiseBy
-import com.android.systemui.util.kotlin.sample
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import javax.inject.Inject
-import kotlin.math.abs
-import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-
-@SysUISingleton
-@OptIn(ExperimentalCoroutinesApi::class)
-class HomeControlsComponentInteractor
-@Inject
-constructor(
-    private val selectedComponentRepository: SelectedComponentRepository,
-    controlsComponent: ControlsComponent,
-    authorizedPanelsRepository: AuthorizedPanelsRepository,
-    userRepository: UserRepository,
-    private val packageChangeInteractor: PackageChangeInteractor,
-    private val systemClock: SystemClock,
-    private val powerManager: PowerManager,
-    private val dreamManager: DreamManager,
-    @Background private val bgScope: CoroutineScope
-) {
-    private val controlsListingController: ControlsListingController? =
-        controlsComponent.getControlsListingController().getOrNull()
-
-    /** Gets the current user's selected panel, or null if there isn't one */
-    private val selectedPanel: Flow<SelectedComponentRepository.SelectedComponent?> =
-        userRepository.selectedUserInfo
-            .flatMapLatest { user ->
-                selectedComponentRepository.selectedComponentFlow(user.userHandle)
-            }
-            .map { if (it?.isPanel == true) it else null }
-
-    /** Gets the current user's authorized panels */
-    private val allAuthorizedPanels: Flow<Set<String>> =
-        userRepository.selectedUserInfo.flatMapLatest { user ->
-            authorizedPanelsRepository.observeAuthorizedPanels(user.userHandle)
-        }
-
-    /** Gets all the available services from [ControlsListingController] */
-    private fun allAvailableServices(): Flow<List<ControlsServiceInfo>> {
-        if (controlsListingController == null) {
-            return emptyFlow()
-        }
-        return conflatedCallbackFlow {
-                val listener =
-                    object : ControlsListingController.ControlsListingCallback {
-                        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
-                            trySend(serviceInfos)
-                        }
-                    }
-                controlsListingController.addCallback(listener)
-                awaitClose { controlsListingController.removeCallback(listener) }
-            }
-            .onStart { emit(controlsListingController.getCurrentServices()) }
-    }
-
-    /** Gets all panels which are available and authorized by the user */
-    private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> =
-        combine(
-            allAvailableServices(),
-            allAuthorizedPanels,
-        ) { serviceInfos, authorizedPanels ->
-            serviceInfos.mapNotNull {
-                val panelActivity = it.panelActivity
-                if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
-                    PanelComponent(it.componentName, panelActivity)
-                } else {
-                    null
-                }
-            }
-        }
-
-    val panelComponent: StateFlow<ComponentName?> =
-        combine(
-                allAvailableAndAuthorizedPanels,
-                selectedPanel,
-            ) { panels, selected ->
-                val item =
-                    panels.firstOrNull { it.componentName == selected?.componentName }
-                        ?: panels.firstOrNull()
-                item?.panelActivity
-            }
-            .stateIn(bgScope, SharingStarted.Eagerly, null)
-
-    private val taskFragmentFinished =
-        MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
-
-    fun onDreamEndUnexpectedly() {
-        powerManager.userActivity(
-            systemClock.uptimeMillis(),
-            PowerManager.USER_ACTIVITY_EVENT_OTHER,
-            PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS,
-        )
-        taskFragmentFinished.tryEmit(systemClock.currentTimeMillis())
-    }
-
-    /**
-     * Monitors if the current home panel package is updated and causes the dream to finish, and
-     * attempts to restart the dream in this case.
-     */
-    @SuppressLint("MissingPermission")
-    suspend fun monitorUpdatesAndRestart() {
-        taskFragmentFinished.resetReplayCache()
-        panelComponent
-            .flatMapLatest { component ->
-                if (component == null) return@flatMapLatest emptyFlow()
-                packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName)
-            }
-            .filter { it.isUpdate() }
-            // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished.
-            .pairwiseBy(::validateUpdatePair)
-            .filterNotNull()
-            .sample(taskFragmentFinished, ::Pair)
-            .filter { (updateStarted, lastFinishedTimestamp) ->
-                abs(updateStarted.timeMillis - lastFinishedTimestamp) <=
-                    MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds
-            }
-            .collect { dreamManager.startDream() }
-    }
-
-    private data class PanelComponent(
-        val componentName: ComponentName,
-        val panelActivity: ComponentName,
-    )
-
-    companion object {
-        /**
-         * The maximum delay between a package update **starting** and the task fragment finishing
-         * which causes us to correlate the package update as the cause of the task fragment
-         * finishing.
-         */
-        val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds
-    }
-}
-
-private fun PackageChangeModel.isUpdate() =
-    this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished
-
-private fun validateUpdatePair(
-    updateStarted: PackageChangeModel,
-    updateFinished: PackageChangeModel
-): PackageChangeModel.UpdateStarted? =
-    when {
-        !updateStarted.isSamePackage(updateFinished) -> null
-        updateStarted !is PackageChangeModel.UpdateStarted -> null
-        updateFinished !is PackageChangeModel.UpdateFinished -> null
-        else -> updateStarted
-    }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt
new file mode 100644
index 0000000..2bcfea8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.stateIn
+
+/** Class to wrap [IHomeControlsRemoteProxy], which exposes the current user's home controls info */
+class HomeControlsRemoteProxy
+@AssistedInject
+constructor(
+    @Background bgScope: CoroutineScope,
+    dumpManager: DumpManager,
+    @Assisted private val proxy: IHomeControlsRemoteProxy,
+) : FlowDumperImpl(dumpManager) {
+
+    private companion object {
+        const val TAG = "HomeControlsRemoteProxy"
+    }
+
+    val componentInfo: Flow<HomeControlsComponentInfo> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : IOnControlsSettingsChangeListener.Stub() {
+                        override fun onControlsSettingsChanged(
+                            panelComponent: ComponentName?,
+                            allowTrivialControlsOnLockscreen: Boolean,
+                        ) {
+                            trySendWithFailureLogging(
+                                HomeControlsComponentInfo(
+                                    panelComponent,
+                                    allowTrivialControlsOnLockscreen,
+                                ),
+                                TAG,
+                            )
+                        }
+                    }
+                proxy.registerListenerForCurrentUser(listener)
+                awaitClose { proxy.unregisterListenerForCurrentUser(listener) }
+            }
+            .distinctUntilChanged()
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+            .dumpValue("componentInfo")
+            .filterNotNull()
+
+    @AssistedFactory
+    interface Factory {
+        fun create(proxy: IHomeControlsRemoteProxy): HomeControlsRemoteProxy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt
new file mode 100644
index 0000000..b14903d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.PersistentConnectionManager
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Queries a remote service for [HomeControlsComponentInfo] necessary to show the home controls
+ * dream.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class RemoteHomeControlsDataSourceDelegator
+@Inject
+constructor(
+    @Background bgScope: CoroutineScope,
+    serviceFactory: HomeControlsRemoteServiceComponent.Factory,
+    @DreamLog logBuffer: LogBuffer,
+    dumpManager: DumpManager,
+) : HomeControlsDataSource, FlowDumperImpl(dumpManager) {
+    private val logger = DreamLogger(logBuffer, TAG)
+
+    private val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy> by lazy {
+        serviceFactory.create(callback).connectionManager
+    }
+
+    private val proxyState =
+        MutableStateFlow<HomeControlsRemoteProxy?>(null)
+            .apply {
+                subscriptionCount
+                    .map { it > 0 }
+                    .dropWhile { !it }
+                    .distinctUntilChanged()
+                    .onEach { active ->
+                        logger.d({ "Remote service connection active: $bool1" }) { bool1 = active }
+                        if (active) {
+                            connectionManager.start()
+                        } else {
+                            connectionManager.stop()
+                        }
+                    }
+                    .launchIn(bgScope)
+            }
+            .dumpValue("proxyState")
+
+    private val callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy> =
+        object : ObservableServiceConnection.Callback<HomeControlsRemoteProxy> {
+            override fun onConnected(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>?,
+                proxy: HomeControlsRemoteProxy,
+            ) {
+                logger.d("Service connected")
+                proxyState.value = proxy
+            }
+
+            override fun onDisconnected(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>?,
+                reason: Int,
+            ) {
+                logger.d({ "Service disconnected with reason $int1" }) { int1 = reason }
+                proxyState.value = null
+            }
+        }
+
+    override val componentInfo: Flow<HomeControlsComponentInfo> =
+        proxyState
+            .filterNotNull()
+            .flatMapLatest { proxy: HomeControlsRemoteProxy -> proxy.componentInfo }
+            .dumpWhileCollecting("componentInfo")
+
+    private companion object {
+        const val TAG = "HomeControlsRemoteDataSourceDelegator"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
rename to packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt
index d547de2..67de30c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt
@@ -14,29 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.systemui.dreams.homecontrols
+package com.android.systemui.dreams.homecontrols.service
 
 import android.app.Activity
-import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration
 import android.content.Intent
 import android.graphics.Rect
 import android.os.Binder
 import android.window.TaskFragmentCreationParams
 import android.window.TaskFragmentInfo
 import android.window.TaskFragmentOperation
-import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
-import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
 import android.window.TaskFragmentOrganizer
-import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE
-import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE
-import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN
 import android.window.TaskFragmentTransaction
-import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK
-import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED
-import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR
-import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED
-import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED
-import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED
 import android.window.WindowContainerTransaction
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -65,7 +54,7 @@
             activity: Activity,
             @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback,
             @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback,
-            hide: () -> Unit
+            hide: () -> Unit,
         ): TaskFragmentComponent
     }
 
@@ -90,26 +79,28 @@
             change.taskFragmentInfo?.let { taskFragmentInfo ->
                 if (taskFragmentInfo.fragmentToken == fragmentToken) {
                     when (change.type) {
-                        TYPE_TASK_FRAGMENT_APPEARED -> {
+                        TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED -> {
                             resultT.addTaskFragmentOperation(
                                 fragmentToken,
-                                TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK)
-                                    .build()
+                                TaskFragmentOperation.Builder(
+                                        TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
+                                    )
+                                    .build(),
                             )
 
                             onCreateCallback(taskFragmentInfo)
                         }
-                        TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
+                        TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
                             onInfoChangedCallback(taskFragmentInfo)
                         }
-                        TYPE_TASK_FRAGMENT_VANISHED -> {
+                        TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED -> {
                             hide()
                         }
-                        TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
-                        TYPE_TASK_FRAGMENT_ERROR -> {
+                        TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
+                        TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR -> {
                             hide()
                         }
-                        TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
+                        TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
                         else ->
                             throw IllegalArgumentException(
                                 "Unknown TaskFragmentEvent=" + change.type
@@ -121,8 +112,8 @@
         organizer.onTransactionHandled(
             transaction.transactionToken,
             resultT,
-            TASK_FRAGMENT_TRANSIT_CHANGE,
-            false
+            TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE,
+            false,
         )
     }
 
@@ -132,15 +123,15 @@
             TaskFragmentCreationParams.Builder(
                     organizer.organizerToken,
                     fragmentToken,
-                    activity.activityToken!!
+                    activity.activityToken!!,
                 )
                 .setInitialRelativeBounds(Rect())
-                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
                 .build()
         organizer.applyTransaction(
             WindowContainerTransaction().createTaskFragment(fragmentOptions),
-            TASK_FRAGMENT_TRANSIT_CHANGE,
-            false
+            TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE,
+            false,
         )
     }
 
@@ -151,8 +142,8 @@
     fun startActivityInTaskFragment(intent: Intent) {
         organizer.applyTransaction(
             WindowContainerTransaction().startActivity(intent),
-            TASK_FRAGMENT_TRANSIT_OPEN,
-            false
+            TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN,
+            false,
         )
     }
 
@@ -162,10 +153,13 @@
             WindowContainerTransaction()
                 .addTaskFragmentOperation(
                     fragmentToken,
-                    TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build()
+                    TaskFragmentOperation.Builder(
+                            TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
+                        )
+                        .build(),
                 ),
-            TASK_FRAGMENT_TRANSIT_CLOSE,
-            false
+            TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE,
+            false,
         )
         organizer.unregisterOrganizer()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl
new file mode 100644
index 0000000..115b62c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl
@@ -0,0 +1,9 @@
+package com.android.systemui.dreams.homecontrols.shared;
+
+import android.os.IRemoteCallback;
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener;
+
+oneway interface IHomeControlsRemoteProxy {
+    void registerListenerForCurrentUser(in IOnControlsSettingsChangeListener callback);
+    void unregisterListenerForCurrentUser(in IOnControlsSettingsChangeListener callback);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl
new file mode 100644
index 0000000..99e5fae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl
@@ -0,0 +1,7 @@
+package com.android.systemui.dreams.homecontrols.shared;
+
+import android.content.ComponentName;
+
+oneway interface IOnControlsSettingsChangeListener {
+    void onControlsSettingsChanged(in ComponentName panelComponent, boolean allowTrivialControlsOnLockscreen);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt
new file mode 100644
index 0000000..b9e5080
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.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.dreams.homecontrols.shared.model
+
+import android.content.ComponentName
+
+data class HomeControlsComponentInfo(
+    val componentName: ComponentName?,
+    val allowTrivialControlsOnLockscreen: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt
new file mode 100644
index 0000000..8187c54
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.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.dreams.homecontrols.shared.model
+
+import kotlinx.coroutines.flow.Flow
+
+/** Source of data for home controls dream to get the necessary information it needs. */
+interface HomeControlsDataSource {
+    val componentInfo: Flow<HomeControlsComponentInfo>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
rename to packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt
index 03f58ac..644d5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt
@@ -14,24 +14,28 @@
  * limitations under the License.
  */
 
-package com.android.systemui.dreams.homecontrols
+package com.android.systemui.dreams.homecontrols.system
 
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.PackageManager
 import android.service.controls.flags.Flags.homePanelDream
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.homeControlsDreamHsum
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamService
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.settings.UserContextProvider
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 class HomeControlsDreamStartable
 @Inject
 constructor(
     context: Context,
-    private val packageManager: PackageManager,
+    private val systemPackageManager: PackageManager,
+    private val userContextProvider: UserContextProvider,
     private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
     @Background private val bgScope: CoroutineScope,
 ) : CoreStartable {
@@ -57,10 +61,16 @@
             } else {
                 PackageManager.COMPONENT_ENABLED_STATE_DISABLED
             }
+        val packageManager =
+            if (homeControlsDreamHsum()) {
+                userContextProvider.userContext.packageManager
+            } else {
+                systemPackageManager
+            }
         packageManager.setComponentEnabledSetting(
             componentName,
             packageState,
-            PackageManager.DONT_KILL_APP
+            PackageManager.DONT_KILL_APP,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt
new file mode 100644
index 0000000..a65d216
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.dreams.homecontrols.system
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.IBinder
+import android.os.RemoteCallbackList
+import android.os.RemoteException
+import android.util.Log
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.concurrent.atomic.AtomicInteger
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.launch
+
+/**
+ * Service which exports the current home controls component name, for use in SystemUI processes
+ * running in other users. This service should only run in the system user.
+ */
+class HomeControlsRemoteService
+@Inject
+constructor(binderFactory: HomeControlsRemoteServiceBinder.Factory) : LifecycleService() {
+    val binder by lazy { binderFactory.create(this) }
+
+    override fun onBind(intent: Intent): IBinder? {
+        super.onBind(intent)
+        return binder
+    }
+}
+
+class HomeControlsRemoteServiceBinder
+@AssistedInject
+constructor(
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    private val controlsSettingsRepository: ControlsSettingsRepository,
+    @Background private val bgContext: CoroutineContext,
+    @DreamLog logBuffer: LogBuffer,
+    @Assisted lifecycleOwner: LifecycleOwner,
+) : IHomeControlsRemoteProxy.Stub(), LifecycleOwner by lifecycleOwner {
+    private val logger = DreamLogger(logBuffer, TAG)
+    private val callbacks =
+        object : RemoteCallbackList<IOnControlsSettingsChangeListener>() {
+            override fun onCallbackDied(listener: IOnControlsSettingsChangeListener?) {
+                if (callbackCount.decrementAndGet() == 0) {
+                    logger.d("Cancelling collection due to callback death")
+                    collectionJob?.cancel()
+                    collectionJob = null
+                }
+            }
+        }
+    private val callbackCount = AtomicInteger(0)
+    private var collectionJob: Job? = null
+
+    override fun registerListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) {
+        if (listener == null) return
+        logger.d("Register listener")
+        val registered = callbacks.register(listener)
+        if (registered && callbackCount.getAndIncrement() == 0) {
+            // If the first listener, start the collection job. This will also take
+            // care of notifying the listener of the initial state.
+            logger.d("Starting collection")
+            collectionJob =
+                lifecycleScope.launch(bgContext) {
+                    combine(
+                            homeControlsComponentInteractor.panelComponent,
+                            controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen,
+                        ) { panelComponent, allowTrivialControls ->
+                            callbacks.notifyAllCallbacks(panelComponent, allowTrivialControls)
+                        }
+                        .launchIn(this)
+                }
+        } else if (registered) {
+            // If not the first listener, notify the listener of the current value immediately.
+            listener.notify(
+                homeControlsComponentInteractor.panelComponent.value,
+                controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value,
+            )
+        }
+    }
+
+    override fun unregisterListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) {
+        if (listener == null) return
+        logger.d("Unregister listener")
+        if (callbacks.unregister(listener) && callbackCount.decrementAndGet() == 0) {
+            logger.d("Cancelling collection due to unregister")
+            collectionJob?.cancel()
+            collectionJob = null
+        }
+    }
+
+    private companion object {
+        const val TAG = "HomeControlsRemoteServiceBinder"
+    }
+
+    private fun IOnControlsSettingsChangeListener.notify(
+        panelComponent: ComponentName?,
+        allowTrivialControlsOnLockscreen: Boolean,
+    ) {
+        try {
+            onControlsSettingsChanged(panelComponent, allowTrivialControlsOnLockscreen)
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Error notifying callback", e)
+        }
+    }
+
+    private fun RemoteCallbackList<IOnControlsSettingsChangeListener>.notifyAllCallbacks(
+        panelComponent: ComponentName?,
+        allowTrivialControlsOnLockscreen: Boolean,
+    ) {
+        val itemCount = beginBroadcast()
+        try {
+            for (i in 0 until itemCount) {
+                getBroadcastItem(i).notify(panelComponent, allowTrivialControlsOnLockscreen)
+            }
+        } finally {
+            finishBroadcast()
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(lifecycleOwner: LifecycleOwner): HomeControlsRemoteServiceBinder
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt
new file mode 100644
index 0000000..ca255fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.dreams.homecontrols.system
+
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Queries local data sources for the [HomeControlsComponentInfo] necessary to show the home
+ * controls dream.
+ */
+@SysUISingleton
+class LocalHomeControlsDataSourceDelegator
+@Inject
+constructor(
+    homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    controlsSettingsRepository: ControlsSettingsRepository,
+) : HomeControlsDataSource {
+    override val componentInfo: Flow<HomeControlsComponentInfo> =
+        combine(
+            homeControlsComponentInteractor.panelComponent,
+            controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen,
+        ) { panelComponent, allowActionOnTrivialControlsInLockscreen ->
+            HomeControlsComponentInfo(panelComponent, allowActionOnTrivialControlsInLockscreen)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt
new file mode 100644
index 0000000..31bd708
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.dreams.homecontrols.system.domain.interactor
+
+import android.content.ComponentName
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class HomeControlsComponentInteractor
+@Inject
+constructor(
+    private val selectedComponentRepository: SelectedComponentRepository,
+    controlsComponent: ControlsComponent,
+    authorizedPanelsRepository: AuthorizedPanelsRepository,
+    userRepository: UserRepository,
+    @Background private val bgScope: CoroutineScope,
+) {
+    private val controlsListingController: ControlsListingController? =
+        controlsComponent.getControlsListingController().getOrNull()
+
+    /** Gets the current user's selected panel, or null if there isn't one */
+    private val selectedPanel: Flow<SelectedComponentRepository.SelectedComponent?> =
+        userRepository.selectedUserInfo
+            .flatMapLatest { user ->
+                selectedComponentRepository.selectedComponentFlow(user.userHandle)
+            }
+            .map { if (it?.isPanel == true) it else null }
+
+    /** Gets the current user's authorized panels */
+    private val allAuthorizedPanels: Flow<Set<String>> =
+        userRepository.selectedUserInfo.flatMapLatest { user ->
+            authorizedPanelsRepository.observeAuthorizedPanels(user.userHandle)
+        }
+
+    /** Gets all the available services from [ControlsListingController] */
+    private fun allAvailableServices(): Flow<List<ControlsServiceInfo>> {
+        if (controlsListingController == null) {
+            return emptyFlow()
+        }
+        return conflatedCallbackFlow {
+                val listener =
+                    object : ControlsListingController.ControlsListingCallback {
+                        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                            trySend(serviceInfos)
+                        }
+                    }
+                controlsListingController.addCallback(listener)
+                awaitClose { controlsListingController.removeCallback(listener) }
+            }
+            .onStart { emit(controlsListingController.getCurrentServices()) }
+    }
+
+    /** Gets all panels which are available and authorized by the user */
+    private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> =
+        combine(allAvailableServices(), allAuthorizedPanels) { serviceInfos, authorizedPanels ->
+            serviceInfos.mapNotNull {
+                val panelActivity = it.panelActivity
+                if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+                    PanelComponent(it.componentName, panelActivity)
+                } else {
+                    null
+                }
+            }
+        }
+
+    val panelComponent: StateFlow<ComponentName?> =
+        combine(allAvailableAndAuthorizedPanels, selectedPanel) { panels, selected ->
+                val item =
+                    panels.firstOrNull { it.componentName == selected?.componentName }
+                        ?: panels.firstOrNull()
+                item?.panelActivity
+            }
+            .stateIn(bgScope, SharingStarted.Eagerly, null)
+
+    private data class PanelComponent(
+        val componentName: ComponentName,
+        val panelActivity: ComponentName,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
index 6dd56de..1b044de 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
@@ -77,7 +77,11 @@
             }
         }
         val buttonAlpha by animateFloatAsState(if (actionState is Finished) 1f else 0f)
-        DoneButton(onDoneButtonClicked, Modifier.graphicsLayer { alpha = buttonAlpha })
+        DoneButton(
+            onDoneButtonClicked = onDoneButtonClicked,
+            modifier = Modifier.graphicsLayer { alpha = buttonAlpha },
+            enabled = actionState is Finished,
+        )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt
index 01ad585..202dba3 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt
@@ -28,13 +28,17 @@
 import com.android.systemui.res.R
 
 @Composable
-fun DoneButton(onDoneButtonClicked: () -> Unit, modifier: Modifier = Modifier) {
+fun DoneButton(
+    onDoneButtonClicked: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+) {
     Row(
         horizontalArrangement = Arrangement.End,
         verticalAlignment = Alignment.CenterVertically,
-        modifier = modifier.fillMaxWidth()
+        modifier = modifier.fillMaxWidth(),
     ) {
-        Button(onClick = onDoneButtonClicked) {
+        Button(onClick = onDoneButtonClicked, enabled = enabled) {
             Text(stringResource(R.string.touchpad_tutorial_done_button))
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index d0a40ec..7638079 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -61,8 +61,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransitionModule;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModelModule;
 import com.android.systemui.log.SessionTracker;
@@ -239,12 +237,6 @@
 
     /** */
     @Provides
-    static KeyguardQuickAffordancesMetricsLogger providesKeyguardQuickAffordancesMetricsLogger() {
-        return new KeyguardQuickAffordancesMetricsLoggerImpl();
-    }
-
-    /** */
-    @Provides
     @SysUISingleton
     static ThreadAssert providesThreadAssert() {
         return new ThreadAssert();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index e68d799..4d999df 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -106,7 +106,7 @@
                                     trySendWithFailureLogging(
                                         getFpSensorType(),
                                         TAG,
-                                        "onAllAuthenticatorsRegistered, emitting fpSensorType"
+                                        "onAllAuthenticatorsRegistered, emitting fpSensorType",
                                     )
                             }
                         }
@@ -114,7 +114,7 @@
                     trySendWithFailureLogging(
                         getFpSensorType(),
                         TAG,
-                        "initial value for fpSensorType"
+                        "initial value for fpSensorType",
                     )
                     awaitClose { authController.removeCallback(callback) }
                 }
@@ -134,7 +134,7 @@
                         trySendWithFailureLogging(
                             keyguardUpdateMonitor.isFingerprintLockedOut,
                             TAG,
-                            "onLockedOutStateChanged"
+                            "onLockedOutStateChanged",
                         )
                     }
                 val callback =
@@ -154,7 +154,7 @@
             .stateIn(
                 scope,
                 started = Eagerly,
-                initialValue = keyguardUpdateMonitor.isFingerprintLockedOut
+                initialValue = keyguardUpdateMonitor.isFingerprintLockedOut,
             )
     }
 
@@ -165,13 +165,13 @@
                         object : KeyguardUpdateMonitorCallback() {
                             override fun onBiometricRunningStateChanged(
                                 running: Boolean,
-                                biometricSourceType: BiometricSourceType?
+                                biometricSourceType: BiometricSourceType?,
                             ) {
                                 if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
                                     trySendWithFailureLogging(
                                         running,
                                         TAG,
-                                        "Fingerprint running state changed"
+                                        "Fingerprint running state changed",
                                     )
                                 }
                             }
@@ -180,7 +180,7 @@
                     trySendWithFailureLogging(
                         keyguardUpdateMonitor.isFingerprintDetectionRunning,
                         TAG,
-                        "Initial fingerprint running state"
+                        "Initial fingerprint running state",
                     )
                     awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
                 }
@@ -193,11 +193,7 @@
             .map { it.isEngaged }
             .filterNotNull()
             .map { it }
-            .stateIn(
-                scope = scope,
-                started = WhileSubscribed(),
-                initialValue = false,
-            )
+            .stateIn(scope = scope, started = WhileSubscribed(), initialValue = false)
 
     // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages
     //  in BiometricStatusRepository
@@ -232,10 +228,7 @@
                             ) {
                                 sendUpdateIfFingerprint(
                                     biometricSourceType,
-                                    ErrorFingerprintAuthenticationStatus(
-                                        msgId,
-                                        errString,
-                                    ),
+                                    ErrorFingerprintAuthenticationStatus(msgId, errString),
                                 )
                             }
 
@@ -246,15 +239,12 @@
                             ) {
                                 sendUpdateIfFingerprint(
                                     biometricSourceType,
-                                    HelpFingerprintAuthenticationStatus(
-                                        msgId,
-                                        helpString,
-                                    ),
+                                    HelpFingerprintAuthenticationStatus(msgId, helpString),
                                 )
                             }
 
                             override fun onBiometricAuthFailed(
-                                biometricSourceType: BiometricSourceType,
+                                biometricSourceType: BiometricSourceType
                             ) {
                                 sendUpdateIfFingerprint(
                                     biometricSourceType,
@@ -270,14 +260,14 @@
                                     biometricSourceType,
                                     AcquiredFingerprintAuthenticationStatus(
                                         AuthenticationReason.DeviceEntryAuthentication,
-                                        acquireInfo
+                                        acquireInfo,
                                     ),
                                 )
                             }
 
                             private fun sendUpdateIfFingerprint(
                                 biometricSourceType: BiometricSourceType,
-                                authenticationStatus: FingerprintAuthenticationStatus
+                                authenticationStatus: FingerprintAuthenticationStatus,
                             ) {
                                 if (biometricSourceType != BiometricSourceType.FINGERPRINT) {
                                     return
@@ -285,13 +275,14 @@
                                 trySendWithFailureLogging(
                                     authenticationStatus,
                                     TAG,
-                                    "new fingerprint authentication status"
+                                    "new fingerprint authentication status",
                                 )
                             }
                         }
                     keyguardUpdateMonitor.registerCallback(callback)
                     awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
                 }
+                .flowOn(mainDispatcher)
                 .buffer(capacity = 4)
 
     override val shouldUpdateIndicatorVisibility: Flow<Boolean> =
@@ -302,7 +293,7 @@
                             shouldUpdateIndicatorVisibility,
                             TAG,
                             "Error sending shouldUpdateIndicatorVisibility " +
-                                "$shouldUpdateIndicatorVisibility"
+                                "$shouldUpdateIndicatorVisibility",
                         )
                     }
 
@@ -310,7 +301,7 @@
                     object : KeyguardUpdateMonitorCallback() {
                         override fun onBiometricRunningStateChanged(
                             running: Boolean,
-                            biometricSourceType: BiometricSourceType?
+                            biometricSourceType: BiometricSourceType?,
                         ) {
                             sendShouldUpdateIndicatorVisibility(true)
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt
new file mode 100644
index 0000000..be4ab4b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.data.repository
+
+import android.annotation.IntDef
+import android.content.res.Resources
+import android.provider.Settings
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.keyguard.shared.model.DevicePosture.UNKNOWN
+import com.android.systemui.res.R
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class KeyguardBypassRepository
+@Inject
+constructor(
+    @Main resources: Resources,
+    biometricSettingsRepository: BiometricSettingsRepository,
+    devicePostureRepository: DevicePostureRepository,
+    dumpManager: DumpManager,
+    private val tunerService: TunerService,
+    @Background backgroundDispatcher: CoroutineDispatcher,
+) : FlowDumperImpl(dumpManager) {
+
+    @get:BypassOverride
+    private val bypassOverride: Int by lazy {
+        resources.getInteger(R.integer.config_face_unlock_bypass_override)
+    }
+
+    private val configFaceAuthSupportedPosture: DevicePosture by lazy {
+        DevicePosture.toPosture(resources.getInteger(R.integer.config_face_auth_supported_posture))
+    }
+
+    private val dismissByDefault: Int by lazy {
+        if (resources.getBoolean(com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) {
+            1
+        } else {
+            0
+        }
+    }
+
+    private var bypassEnabledSetting: Flow<Boolean> =
+        callbackFlow {
+                val updateBypassSetting = { state: Boolean ->
+                    trySendWithFailureLogging(state, TAG, "Error sending bypassSetting $state")
+                }
+
+                val tunable =
+                    TunerService.Tunable { key, _ ->
+                        updateBypassSetting(tunerService.getValue(key, dismissByDefault) != 0)
+                    }
+
+                updateBypassSetting(false)
+                tunerService.addTunable(tunable, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD)
+                awaitClose { tunerService.removeTunable(tunable) }
+            }
+            .flowOn(backgroundDispatcher)
+            .dumpWhileCollecting("bypassEnabledSetting")
+
+    val overrideFaceBypassSetting: Flow<Boolean> =
+        when (bypassOverride) {
+            FACE_UNLOCK_BYPASS_ALWAYS -> flowOf(true)
+            FACE_UNLOCK_BYPASS_NEVER -> flowOf(false)
+            else -> bypassEnabledSetting
+        }
+
+    val isPostureAllowedForFaceAuth: Flow<Boolean> =
+        when (configFaceAuthSupportedPosture) {
+            UNKNOWN -> flowOf(true)
+            else ->
+                devicePostureRepository.currentDevicePosture
+                    .map { posture -> posture == configFaceAuthSupportedPosture }
+                    .distinctUntilChanged()
+        }
+
+    /**
+     * Whether bypass is available.
+     *
+     * Bypass is the ability to skip the lockscreen when the device is unlocked using non-primary
+     * authentication types like face unlock, instead of requiring the user to explicitly dismiss
+     * the lockscreen by swiping after the device is already unlocked.
+     *
+     * "Available" refers to a combination of the user setting to skip the lockscreen being set,
+     * whether hard-wired OEM-overridable configs allow the feature, whether a foldable is in the
+     * right foldable posture, and other such things. It does _not_ model this based on more
+     * runtime-like states of the UI.
+     */
+    val isBypassAvailable: Flow<Boolean> =
+        combine(
+                overrideFaceBypassSetting,
+                biometricSettingsRepository.isFaceAuthEnrolledAndEnabled,
+                isPostureAllowedForFaceAuth,
+            ) {
+                bypassOverride: Boolean,
+                isFaceEnrolledAndEnabled: Boolean,
+                isPostureAllowedForFaceAuth: Boolean ->
+                bypassOverride && isFaceEnrolledAndEnabled && isPostureAllowedForFaceAuth
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("isBypassAvailable")
+
+    @IntDef(FACE_UNLOCK_BYPASS_NO_OVERRIDE, FACE_UNLOCK_BYPASS_ALWAYS, FACE_UNLOCK_BYPASS_NEVER)
+    @Retention(AnnotationRetention.SOURCE)
+    private annotation class BypassOverride
+
+    companion object {
+        private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0
+        private const val FACE_UNLOCK_BYPASS_ALWAYS = 1
+        private const val FACE_UNLOCK_BYPASS_NEVER = 2
+
+        private const val TAG = "KeyguardBypassRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index d49550e..d0de21b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -36,12 +36,14 @@
 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.kotlin.FlowDumperImpl
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -64,7 +66,14 @@
     configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
     dumpManager: DumpManager,
     userHandle: UserHandle,
-) {
+) : FlowDumperImpl(dumpManager) {
+    /**
+     * Whether a quick affordance is being launched. Quick Affordances are interactive lockscreen UI
+     * elements that allow the user to perform quick actions without unlocking their device.
+     */
+    val launchingAffordance: MutableStateFlow<Boolean> =
+        MutableStateFlow(false).dumpValue("launchingAffordance")
+
     // Configs for all keyguard quick affordances, mapped by the quick affordance ID as key
     private val configsByAffordanceId: Map<String, KeyguardQuickAffordanceConfig> =
         configs.associateBy { it.key }
@@ -112,11 +121,7 @@
                     }
                 }
             }
-            .stateIn(
-                scope = scope,
-                started = SharingStarted.Eagerly,
-                initialValue = emptyMap(),
-            )
+            .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap())
 
     init {
         legacySettingSyncer.startSyncing()
@@ -144,14 +149,8 @@
      * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
      * IDs should be descending priority order.
      */
-    fun setSelections(
-        slotId: String,
-        affordanceIds: List<String>,
-    ) {
-        selectionManager.value.setSelections(
-            slotId = slotId,
-            affordanceIds = affordanceIds,
-        )
+    fun setSelections(slotId: String, affordanceIds: List<String>) {
+        selectionManager.value.setSelections(slotId = slotId, affordanceIds = affordanceIds)
     }
 
     /**
@@ -222,10 +221,7 @@
             val (slotId, slotCapacity) = parseSlot(unparsedSlot)
             check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" }
             seenSlotIds.add(slotId)
-            KeyguardSlotPickerRepresentation(
-                id = slotId,
-                maxSelectedAffordances = slotCapacity,
-            )
+            KeyguardSlotPickerRepresentation(id = slotId, maxSelectedAffordances = slotCapacity)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt
new file mode 100644
index 0000000..7699bab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class PulseExpansionRepository @Inject constructor(dumpManager: DumpManager) :
+    FlowDumperImpl(dumpManager) {
+    /**
+     * Whether the notification panel is expanding from the user swiping downward on a notification
+     * from the pulsing state, or swiping anywhere on the screen when face bypass is enabled
+     */
+    val isPulseExpanding: MutableStateFlow<Boolean> =
+        MutableStateFlow(false).dumpValue("pulseExpanding")
+}
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 b60e98a..8c60371 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
@@ -19,7 +19,6 @@
 import android.animation.ValueAnimator
 import android.util.MathUtils
 import com.android.app.animation.Interpolators
-import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags.communalSceneKtfRefactor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -39,19 +38,19 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import com.android.systemui.util.kotlin.sample
-import java.util.UUID
-import javax.inject.Inject
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import java.util.UUID
+import javax.inject.Inject
 import com.android.app.tracing.coroutines.launchTraced as launch
 
 @SysUISingleton
@@ -176,98 +175,101 @@
         if (SceneContainerFlag.isEnabled) return
         var transitionId: UUID? = null
         scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
-            shadeRepository.legacyShadeExpansion
-                .sampleCombine(
-                    transitionInteractor.startedKeyguardTransitionStep,
-                    keyguardInteractor.statusBarState,
-                    keyguardInteractor.isKeyguardDismissible,
-                    keyguardInteractor.isKeyguardOccluded,
-                )
-                .collect {
-                    (
-                        shadeExpansion,
-                        startedStep,
-                        statusBarState,
-                        isKeyguardUnlocked,
-                        isKeyguardOccluded) ->
-                    val id = transitionId
-                    val currentTransitionInfo =
-                        internalTransitionInteractor.currentTransitionInfoInternal()
-                    if (id != null) {
-                        if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) {
-                            // An existing `id` means a transition is started, and calls to
-                            // `updateTransition` will control it until FINISHED or CANCELED
-                            var nextState =
-                                if (shadeExpansion == 0f) {
-                                    TransitionState.FINISHED
-                                } else if (shadeExpansion == 1f) {
-                                    TransitionState.CANCELED
-                                } else {
-                                    TransitionState.RUNNING
-                                }
+            shadeRepository.legacyShadeExpansion.collect { shadeExpansion ->
+                val statusBarState = keyguardInteractor.statusBarState.value
+                val isKeyguardUnlocked = keyguardInteractor.isKeyguardDismissible.value
+                val isKeyguardOccluded = keyguardInteractor.isKeyguardOccluded.value
+                val startedStep = transitionInteractor.startedKeyguardTransitionStep.value
 
-                            // startTransition below will issue the CANCELED directly
-                            if (nextState != TransitionState.CANCELED) {
-                                transitionRepository.updateTransition(
-                                    id,
-                                    // This maps the shadeExpansion to a much faster curve, to match
-                                    // the existing logic
-                                    1f -
-                                        MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion),
-                                    nextState,
-                                )
+                val id = transitionId
+                val currentTransitionInfo =
+                    internalTransitionInteractor.currentTransitionInfoInternal()
+                if (id != null) {
+                    if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) {
+                        // An existing `id` means a transition is started, and calls to
+                        // `updateTransition` will control it until FINISHED or CANCELED
+                        var nextState =
+                            if (shadeExpansion == 0f) {
+                                TransitionState.FINISHED
+                            } else if (shadeExpansion == 1f) {
+                                TransitionState.CANCELED
+                            } else {
+                                TransitionState.RUNNING
                             }
 
-                            if (
-                                nextState == TransitionState.CANCELED ||
-                                    nextState == TransitionState.FINISHED
-                            ) {
-                                transitionId = null
-                            }
-
-                            // If canceled, just put the state back
-                            // TODO(b/278086361): This logic should happen in
-                            //  FromPrimaryBouncerInteractor.
-                            if (nextState == TransitionState.CANCELED) {
-                                transitionRepository.startTransition(
-                                    TransitionInfo(
-                                        ownerName =
-                                            "$name " +
-                                                "(on behalf of FromPrimaryBouncerInteractor)",
-                                        from = KeyguardState.PRIMARY_BOUNCER,
-                                        to =
-                                            if (isKeyguardOccluded) KeyguardState.OCCLUDED
-                                            else KeyguardState.LOCKSCREEN,
-                                        modeOnCanceled = TransitionModeOnCanceled.REVERSE,
-                                        animator =
-                                            getDefaultAnimatorForTransitionsToState(
-                                                    KeyguardState.LOCKSCREEN
-                                                )
-                                                .apply { duration = 100L },
-                                    )
-                                )
-                            }
+                        // startTransition below will issue the CANCELED directly
+                        if (nextState != TransitionState.CANCELED) {
+                            transitionRepository.updateTransition(
+                                id,
+                                // This maps the shadeExpansion to a much faster curve, to match
+                                // the existing logic
+                                1f - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion),
+                                nextState,
+                            )
                         }
-                    } else {
-                        // TODO (b/251849525): Remove statusbarstate check when that state is
-                        // integrated into KeyguardTransitionRepository
+
                         if (
-                            // Use currentTransitionInfo to decide whether to start the transition.
-                            currentTransitionInfo.to == KeyguardState.LOCKSCREEN &&
-                                shadeExpansion > 0f &&
-                                shadeExpansion < 1f &&
-                                shadeRepository.legacyShadeTracking.value &&
-                                !isKeyguardUnlocked &&
-                                statusBarState == KEYGUARD
+                            nextState == TransitionState.CANCELED ||
+                                nextState == TransitionState.FINISHED
                         ) {
-                            transitionId =
-                                startTransitionTo(
-                                    toState = KeyguardState.PRIMARY_BOUNCER,
-                                    animator = null, // transition will be manually controlled,
-                                    ownerReason = "#listenForLockscreenToPrimaryBouncerDragging",
+                            transitionId = null
+                        }
+
+                        // If canceled, just put the state back
+                        // TODO(b/278086361): This logic should happen in
+                        //  FromPrimaryBouncerInteractor.
+                        if (nextState == TransitionState.CANCELED) {
+                            transitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName =
+                                        "$name " + "(on behalf of FromPrimaryBouncerInteractor)",
+                                    from = KeyguardState.PRIMARY_BOUNCER,
+                                    to =
+                                        if (isKeyguardOccluded) KeyguardState.OCCLUDED
+                                        else KeyguardState.LOCKSCREEN,
+                                    modeOnCanceled = TransitionModeOnCanceled.REVERSE,
+                                    animator =
+                                        getDefaultAnimatorForTransitionsToState(
+                                                KeyguardState.LOCKSCREEN
+                                            )
+                                            .apply { duration = 100L },
                                 )
+                            )
                         }
                     }
+                } else {
+                    // TODO (b/251849525): Remove statusbarstate check when that state is
+                    // integrated into KeyguardTransitionRepository
+                    if (
+                        // Use currentTransitionInfo to decide whether to start the transition.
+                        currentTransitionInfo.to == KeyguardState.LOCKSCREEN &&
+                            shadeExpansion > 0f &&
+                            shadeExpansion < 1f &&
+                            shadeRepository.legacyShadeTracking.value &&
+                            !isKeyguardUnlocked &&
+                            statusBarState == KEYGUARD
+                    ) {
+                        transitionId =
+                            startTransitionTo(
+                                toState = KeyguardState.PRIMARY_BOUNCER,
+                                animator = null, // transition will be manually controlled,
+                                ownerReason = "#listenForLockscreenToPrimaryBouncerDragging",
+                            )
+                    }
+                }
+            }
+        }
+
+        // Ensure that transitionId is nulled out if external signals cause a PRIMARY_BOUNCER
+        // transition to be canceled.
+        scope.launch {
+            transitionInteractor.transitions
+                .filter {
+                    it.transitionState == TransitionState.CANCELED &&
+                        it.to == KeyguardState.PRIMARY_BOUNCER
+                }
+                .collect {
+                    transitionId = null
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt
new file mode 100644
index 0000000..d793064
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.KeyguardBypassRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.combine
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class KeyguardBypassInteractor
+@Inject
+constructor(
+    keyguardBypassRepository: KeyguardBypassRepository,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
+    keyguardQuickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
+    pulseExpansionInteractor: PulseExpansionInteractor,
+    sceneInteractor: SceneInteractor,
+    shadeInteractor: ShadeInteractor,
+    dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+
+    /**
+     * Whether bypassing the keyguard is enabled by the user in user settings (skipping the
+     * lockscreen when authenticating using secondary authentication types like face unlock).
+     */
+    val isBypassAvailable: Flow<Boolean> =
+        keyguardBypassRepository.isBypassAvailable.dumpWhileCollecting("isBypassAvailable")
+
+    /**
+     * Models whether bypass is unavailable (no secondary authentication types enrolled), or if the
+     * keyguard can be bypassed as a combination of the settings toggle value set by the user and
+     * other factors related to device state.
+     */
+    val canBypass: Flow<Boolean> =
+        isBypassAvailable
+            .flatMapLatest { isBypassAvailable ->
+                if (isBypassAvailable) {
+                    combine(
+                        sceneInteractor.currentScene.map { scene -> scene == Scenes.Bouncer },
+                        alternateBouncerInteractor.isVisible,
+                        sceneInteractor.currentScene.map { scene -> scene == Scenes.Lockscreen },
+                        keyguardQuickAffordanceInteractor.launchingAffordance,
+                        pulseExpansionInteractor.isPulseExpanding,
+                        shadeInteractor.isQsExpanded,
+                    ) {
+                        isBouncerShowing,
+                        isAlternateBouncerShowing,
+                        isOnLockscreenScene,
+                        isLaunchingAffordance,
+                        isPulseExpanding,
+                        isQsExpanded ->
+                        when {
+                            isBouncerShowing -> true
+                            isAlternateBouncerShowing -> true
+                            !isOnLockscreenScene -> false
+                            isLaunchingAffordance -> false
+                            isPulseExpanding -> false
+                            isQsExpanded -> false
+                            else -> true
+                        }
+                    }
+                } else {
+                    flowOf(false)
+                }
+            }
+            .dumpWhileCollecting("canBypass")
+
+    companion object {
+        private const val TAG: String = "KeyguardBypassInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index b24ca1a..2e0a160 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -284,7 +284,7 @@
         }
 
     /** Observable for the [StatusBarState] */
-    val statusBarState: Flow<StatusBarState> = repository.statusBarState
+    val statusBarState: StateFlow<StatusBarState> = repository.statusBarState
 
     /** Observable for [BiometricUnlockModel] when biometrics are used to unlock the device. */
     val biometricUnlockState: StateFlow<BiometricUnlockModel> = repository.biometricUnlockState
@@ -350,23 +350,21 @@
     val dismissAlpha: Flow<Float> =
         shadeRepository.legacyShadeExpansion
             .sampleCombine(
-                statusBarState,
                 keyguardTransitionInteractor.currentKeyguardState,
                 keyguardTransitionInteractor.transitionState,
                 isKeyguardDismissible,
                 keyguardTransitionInteractor.isFinishedIn(Scenes.Communal, GLANCEABLE_HUB),
             )
-            .filter { (_, _, _, step, _, _) -> !step.transitionState.isTransitioning() }
+            .filter { (_, _, step, _, _) -> !step.transitionState.isTransitioning() }
             .transform {
                 (
                     legacyShadeExpansion,
-                    statusBarState,
                     currentKeyguardState,
                     step,
                     isKeyguardDismissible,
                     onGlanceableHub) ->
                 if (
-                    statusBarState == StatusBarState.KEYGUARD &&
+                    statusBarState.value == StatusBarState.KEYGUARD &&
                         isKeyguardDismissible &&
                         currentKeyguardState == LOCKSCREEN &&
                         legacyShadeExpansion != 1f
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 26bf26b..21afd3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -61,6 +61,8 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
@@ -91,6 +93,11 @@
     @Application private val appContext: Context,
     private val sceneInteractor: Lazy<SceneInteractor>,
 ) {
+    /**
+     * Whether a quick affordance is being launched. Quick Affordances are interactive lockscreen UI
+     * elements that allow the user to perform quick actions without unlocking their device.
+     */
+    val launchingAffordance: StateFlow<Boolean> = repository.get().launchingAffordance.asStateFlow()
 
     /**
      * Whether the UI should use the long press gesture to activate quick affordances.
@@ -167,11 +174,7 @@
      * @param expandable An optional [Expandable] for the activity- or dialog-launch animation
      * @param slotId The id of the lockscreen slot that the affordance is in
      */
-    fun onQuickAffordanceTriggered(
-        configKey: String,
-        expandable: Expandable?,
-        slotId: String,
-    ) {
+    fun onQuickAffordanceTriggered(configKey: String, expandable: Expandable?, slotId: String) {
         val (decodedSlotId, decodedConfigKey) = configKey.decode()
         val config =
             repository.get().selections.value[decodedSlotId]?.find { it.key == decodedConfigKey }
@@ -191,10 +194,7 @@
                 )
             is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit
             is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog ->
-                showDialog(
-                    result.dialog,
-                    result.expandable,
-                )
+                showDialog(result.dialog, result.expandable)
         }
     }
 
@@ -225,12 +225,7 @@
 
         selections.add(affordanceId)
 
-        repository
-            .get()
-            .setSelections(
-                slotId = slotId,
-                affordanceIds = selections,
-            )
+        repository.get().setSelections(slotId = slotId, affordanceIds = selections)
 
         logger.logQuickAffordanceSelected(slotId, affordanceId)
         metricsLogger.logOnShortcutSelected(slotId, affordanceId)
@@ -274,12 +269,7 @@
                 .getOrDefault(slotId, emptyList())
                 .toMutableList()
         return if (selections.remove(affordanceId)) {
-            repository
-                .get()
-                .setSelections(
-                    slotId = slotId,
-                    affordanceIds = selections,
-                )
+            repository.get().setSelections(slotId = slotId, affordanceIds = selections)
             true
         } else {
             false
@@ -399,11 +389,15 @@
                 intent,
                 true /* dismissShade */,
                 expandable?.activityTransitionController(),
-                true /* showOverLockscreenWhenLocked */,
+                true, /* showOverLockscreenWhenLocked */
             )
         }
     }
 
+    fun setLaunchingAffordance(isLaunchingAffordance: Boolean) {
+        repository.get().launchingAffordance.value = isLaunchingAffordance
+    }
+
     private fun String.encode(slotId: String): String {
         return "$slotId$DELIMITER$this"
     }
@@ -444,19 +438,19 @@
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME,
-                value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME)
+                value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME),
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP,
-                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP)
+                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP),
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS,
-                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS)
+                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS),
             ),
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_PREVIEW_ANIMATION,
-                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PREVIEW_ANIMATION)
+                value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PREVIEW_ANIMATION),
             ),
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.kt
new file mode 100644
index 0000000..377d7ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.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.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.PulseExpansionRepository
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@SysUISingleton
+class PulseExpansionInteractor
+@Inject
+constructor(private val repository: PulseExpansionRepository, dumpManager: DumpManager) :
+    FlowDumperImpl(dumpManager) {
+    /**
+     * Whether the notification panel is expanding from the user swiping downward on a notification
+     * from the pulsing state, or swiping anywhere on the screen when face bypass is enabled
+     */
+    val isPulseExpanding: StateFlow<Boolean> =
+        repository.isPulseExpanding.asStateFlow().dumpValue("isPulseExpanding")
+
+    /** Updates whether a pulse expansion is occurring. */
+    fun setPulseExpanding(pulseExpanding: Boolean) {
+        repository.isPulseExpanding.value = pulseExpanding
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
index e404f27..2e3a095 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
@@ -20,13 +20,13 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.shade.data.repository.FlingInfo
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.Utils.Companion.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -39,8 +39,8 @@
 constructor(
     @Background backgroundScope: CoroutineScope,
     shadeRepository: ShadeRepository,
-    transitionInteractor: KeyguardTransitionInteractor,
-    keyguardInteractor: KeyguardInteractor,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    private val keyguardInteractor: KeyguardInteractor,
 ) {
     /**
      * Emits a [FlingInfo] whenever a swipe to dismiss gesture has started a fling animation on the
@@ -50,20 +50,15 @@
      * LOCKSCREEN -> GONE, and by [KeyguardSurfaceBehindInteractor] to match the surface remote
      * animation's velocity to the fling velocity, if applicable.
      */
-    val dismissFling =
+    val dismissFling: StateFlow<FlingInfo?> =
         shadeRepository.currentFling
-            .sample(
-                transitionInteractor.startedKeyguardTransitionStep,
-                keyguardInteractor.isKeyguardDismissible,
-                keyguardInteractor.statusBarState,
-            )
-            .filter { (flingInfo, startedStep, keyguardDismissable, statusBarState) ->
+            .filter { flingInfo ->
                 flingInfo != null &&
                     !flingInfo.expand &&
-                    statusBarState != StatusBarState.SHADE_LOCKED &&
-                    startedStep.to == KeyguardState.LOCKSCREEN &&
-                    keyguardDismissable
+                    keyguardInteractor.statusBarState.value != StatusBarState.SHADE_LOCKED &&
+                    transitionInteractor.startedKeyguardTransitionStep.value.to ==
+                        KeyguardState.LOCKSCREEN &&
+                    keyguardInteractor.isKeyguardDismissible.value
             }
-            .map { (flingInfo, _) -> flingInfo }
             .stateIn(backgroundScope, SharingStarted.Eagerly, null)
 }
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 40d4193..0d81604 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
@@ -27,6 +27,7 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -81,6 +82,7 @@
     private val communalInteractor: CommunalInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
+    private val pulseExpansionInteractor: PulseExpansionInteractor,
     notificationShadeWindowModel: NotificationShadeWindowModel,
     private val aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
@@ -371,7 +373,7 @@
 
     /** Is there an expanded pulse, are we animating in response? */
     private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> {
-        return notificationsKeyguardInteractor.isPulseExpanding
+        return pulseExpansionInteractor.isPulseExpanding
             .pairwise(initialValue = null)
             // If pulsing changes, start animating, unless it's the first emission
             .map { (prev, expanding) -> AnimatableEvent(expanding, startAnimating = prev != null) }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 0b37b5b..1ca3927 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -1146,4 +1146,4 @@
             updateState();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 9c8e84f..bacff99 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -38,12 +38,14 @@
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.togetherWith
 import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement.spacedBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.windowInsetsPadding
@@ -73,13 +75,14 @@
 import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -98,10 +101,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.lifecycle.setSnapshotBinding
-import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.view.MediaHost
-import com.android.systemui.media.dagger.MediaModule.QS_PANEL
-import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
@@ -127,7 +127,6 @@
 import java.io.PrintWriter
 import java.util.function.Consumer
 import javax.inject.Inject
-import javax.inject.Named
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
@@ -135,6 +134,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 
 @SuppressLint("ValidFragment")
 class QSFragmentCompose
@@ -142,11 +142,11 @@
 constructor(
     private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
     private val dumpManager: DumpManager,
-    @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
-    @Named(QS_PANEL) private val qsMediaHost: MediaHost,
 ) : LifecycleFragment(), QS, Dumpable {
 
     private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
+    private val collapsedMediaVisibilityChangedListener =
+        MutableStateFlow<(Consumer<Boolean>)?>(null)
     private val heightListener = MutableStateFlow<QS.HeightListener?>(null)
     private val qsContainerController = MutableStateFlow<QSContainerController?>(null)
 
@@ -183,8 +183,6 @@
         QSComposeFragment.isUnexpectedlyInLegacyMode()
         viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)
 
-        qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
-        qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
         setListenerCollections()
         lifecycleScope.launch { viewModel.activate() }
     }
@@ -491,7 +489,7 @@
     }
 
     override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
-        // TODO (b/353253280)
+        collapsedMediaVisibilityChangedListener.value = listener
     }
 
     override fun setScrollListener(scrollListener: QS.ScrollListener?) {
@@ -534,6 +532,7 @@
             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 this@QSFragmentCompose.view?.setSnapshotBinding {
                     scrollListener.value?.onQsPanelScrollChanged(scrollState.value)
+                    collapsedMediaVisibilityChangedListener.value?.accept(viewModel.qqsMediaVisible)
                 }
                 launch {
                     setListenerJob(
@@ -569,7 +568,7 @@
                 .squishiness
                 .collectAsStateWithLifecycle()
 
-        Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) {
+        Column(modifier = Modifier.sysuiResTag(ResIdTags.quickQsPanel)) {
             Box(
                 modifier =
                     Modifier.fillMaxWidth()
@@ -581,6 +580,9 @@
                                 leftFromRoot + coordinates.size.width,
                                 topFromRoot + coordinates.size.height,
                             )
+                            if (squishiness == 1f) {
+                                viewModel.qqsHeight = coordinates.size.height
+                            }
                         }
                         // Use an approach layout to determien the height without squishiness, as
                         // that's the value that NPVC and QuickSettingsController care about
@@ -595,8 +597,7 @@
                         .padding(top = { qqsPadding }, bottom = { bottomPadding })
             ) {
                 if (viewModel.isQsEnabled) {
-                    QuickQuickSettings(
-                        viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel,
+                    Column(
                         modifier =
                             Modifier.collapseExpandSemanticAction(
                                     stringResource(
@@ -608,7 +609,16 @@
                                         QuickSettingsShade.Dimensions.Padding.roundToPx()
                                     }
                                 ),
-                    )
+                        verticalArrangement =
+                            spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
+                    ) {
+                        QuickQuickSettings(
+                            viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel
+                        )
+                        if (viewModel.qqsMediaVisible) {
+                            MediaObject(mediaHost = viewModel.qqsMediaHost)
+                        }
+                    }
                 }
             }
             Spacer(modifier = Modifier.weight(1f))
@@ -645,14 +655,27 @@
                                 }
                                 .onSizeChanged { viewModel.qsScrollHeight = it.height }
                                 .verticalScroll(scrollState)
+                                .sysuiResTag(ResIdTags.qsScroll)
                     ) {
                         Spacer(
                             modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }
                         )
                         QuickSettingsLayout(
                             viewModel = viewModel.containerViewModel,
-                            modifier = Modifier.sysuiResTag("quick_settings_panel"),
+                            modifier = Modifier.sysuiResTag(ResIdTags.quickSettingsPanel),
                         )
+                        Spacer(modifier = Modifier.height(8.dp))
+                        if (viewModel.qsMediaVisible) {
+                            MediaObject(
+                                mediaHost = viewModel.qsMediaHost,
+                                modifier =
+                                    Modifier.padding(
+                                        horizontal = {
+                                            QuickSettingsShade.Dimensions.Padding.roundToPx()
+                                        }
+                                    ),
+                            )
+                        }
                     }
                 }
                 QuickSettingsTheme {
@@ -660,7 +683,7 @@
                         viewModel = viewModel.footerActionsViewModel,
                         qsVisibilityLifecycleOwner = this@QSFragmentCompose,
                         modifier =
-                            Modifier.sysuiResTag("qs_footer_actions")
+                            Modifier.sysuiResTag(ResIdTags.qsFooterActions)
                                 .element(ElementKeys.FooterActions),
                     )
                 }
@@ -914,3 +937,29 @@
     } else {
         this
     }
+
+@Composable
+private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) {
+    Box {
+        AndroidView(
+            modifier = modifier,
+            factory = {
+                mediaHost.hostView.apply {
+                    layoutParams =
+                        FrameLayout.LayoutParams(
+                            FrameLayout.LayoutParams.MATCH_PARENT,
+                            FrameLayout.LayoutParams.WRAP_CONTENT,
+                        )
+                }
+            },
+            onReset = {},
+        )
+    }
+}
+
+private object ResIdTags {
+    const val quickSettingsPanel = "quick_settings_panel"
+    const val quickQsPanel = "quick_qs_panel"
+    const val qsScroll = "expanded_qs_scroll_view"
+    const val qsFooterActions = "qs_footer_actions"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt
new file mode 100644
index 0000000..5127320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.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.qs.composefragment.dagger
+
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.Utils
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+interface QSFragmentComposeModule {
+
+    companion object {
+        const val QS_USING_MEDIA_PLAYER = "compose_fragment_using_media_player"
+
+        @Provides
+        @SysUISingleton
+        @Named(QS_USING_MEDIA_PLAYER)
+        fun providesUsingMedia(@Application context: Context): Boolean {
+            return Utils.useQsMediaPlayer(context)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index d30c6be..0ca621d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -38,11 +38,17 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.controls.ui.view.MediaHostState
+import com.android.systemui.media.dagger.MediaModule.QS_PANEL
+import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
-import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
@@ -60,10 +66,14 @@
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import java.io.PrintWriter
+import javax.inject.Named
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
@@ -83,7 +93,10 @@
     configurationInteractor: ConfigurationInteractor,
     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
     private val squishinessInteractor: TileSquishinessInteractor,
-    private val paginatedGridViewModel: PaginatedGridViewModel,
+    private val inFirstPageViewModel: InFirstPageViewModel,
+    @Named(QUICK_QS_PANEL) val qqsMediaHost: MediaHost,
+    @Named(QS_PANEL) val qsMediaHost: MediaHost,
+    @Named(QSFragmentComposeModule.QS_USING_MEDIA_PLAYER) private val usingMedia: Boolean,
     @Assisted private val lifecycleScope: LifecycleCoroutineScope,
 ) : Dumpable, ExclusiveActivatable() {
 
@@ -191,7 +204,7 @@
     var collapseExpandAccessibilityAction: Runnable? = null
 
     val inFirstPage: Boolean
-        get() = paginatedGridViewModel.inFirstPage
+        get() = inFirstPageViewModel.inFirstPage
 
     var overScrollAmount by mutableStateOf(0)
 
@@ -222,6 +235,30 @@
         }
     }
 
+    val showingMirror: Boolean
+        get() = containerViewModel.brightnessSliderViewModel.showMirror
+
+    // The initial values in these two are not meaningful. The flow will emit on start the correct
+    // values. This is because we need to lazily fetch them after initMediaHosts.
+    val qqsMediaVisible by
+        hydrator.hydratedStateOf(
+            traceName = "qqsMediaVisible",
+            initialValue = usingMedia,
+            source =
+                if (usingMedia) {
+                    mediaHostVisible(qqsMediaHost)
+                } else {
+                    flowOf(false)
+                },
+        )
+
+    val qsMediaVisible by
+        hydrator.hydratedStateOf(
+            traceName = "qsMediaVisible",
+            initialValue = usingMedia,
+            source = if (usingMedia) mediaHostVisible(qsMediaHost) else flowOf(false),
+        )
+
     private var qsBounds by mutableStateOf(Rect())
 
     private val constrainedSquishinessFraction: Float
@@ -259,9 +296,6 @@
                     .onStart { emit(sysuiStatusBarStateController.state) },
         )
 
-    val showingMirror: Boolean
-        get() = containerViewModel.brightnessSliderViewModel.showMirror
-
     private val isKeyguardState: Boolean
         get() = statusBarState == StatusBarState.KEYGUARD
 
@@ -323,6 +357,7 @@
         )
 
     override suspend fun onActivated(): Nothing {
+        initMediaHosts() // init regardless of using media (same as current QS).
         coroutineScope {
             launch { hydrateSquishinessInteractor() }
             launch { hydrator.activate() }
@@ -331,6 +366,19 @@
         }
     }
 
+    private fun initMediaHosts() {
+        qqsMediaHost.apply {
+            expansion = MediaHostState.EXPANDED
+            showsOnlyActiveMedia = true
+            init(MediaHierarchyManager.LOCATION_QQS)
+        }
+        qsMediaHost.apply {
+            expansion = MediaHostState.EXPANDED
+            showsOnlyActiveMedia = false
+            init(MediaHierarchyManager.LOCATION_QS)
+        }
+    }
+
     private suspend fun hydrateSquishinessInteractor() {
         snapshotFlow { constrainedSquishinessFraction }
             .collect { squishinessInteractor.setSquishinessValue(it) }
@@ -373,6 +421,10 @@
                 println("qqsHeight", "${qqsHeight}px")
                 println("qsScrollHeight", "${qsScrollHeight}px")
             }
+            printSection("Media") {
+                println("qqsMediaVisible", qqsMediaVisible)
+                println("qsMediaVisible", qsMediaVisible)
+            }
         }
     }
 
@@ -390,3 +442,21 @@
 }
 
 private val SHORT_PARALLAX_AMOUNT = 0.1f
+
+/**
+ * Returns a flow to track the visibility of a [MediaHost]. The flow will emit on start the visible
+ * state of the view.
+ */
+private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> {
+    return callbackFlow {
+            val listener: (Boolean) -> Unit = { visible: Boolean -> trySend(visible) }
+            mediaHost.addVisibilityChangeListener(listener)
+
+            awaitClose { mediaHost.removeVisibilityChangeListener(listener) }
+        }
+        // Need to use this to set initial state because on creation of the media host, the
+        // view visibility is not in sync with [MediaHost.visible], which is what we track with
+        // the listener. The correct state is set as part of init, so we need to get the state
+        // lazily.
+        .onStart { emit(mediaHost.visible) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 29bcad4..94b8a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -19,6 +19,7 @@
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.ReduceBrightColorsControllerImpl;
+import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule;
 import com.android.systemui.qs.external.QSExternalModule;
 import com.android.systemui.qs.panels.dagger.PanelsModule;
 import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
@@ -40,6 +41,7 @@
         includes = {
                 MediaModule.class,
                 PanelsModule.class,
+                QSFragmentComposeModule.class,
                 QSExternalModule.class,
                 QSFlagsModule.class,
                 QSHostModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index d55763a..6cc2cbc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -40,9 +40,9 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
 import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
 import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
@@ -54,7 +54,7 @@
 class PaginatedGridLayout
 @Inject
 constructor(
-    private val viewModel: PaginatedGridViewModel,
+    private val viewModelFactory: PaginatedGridViewModel.Factory,
     @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
 ) : GridLayout by delegateGridLayout {
     @Composable
@@ -63,13 +63,18 @@
         modifier: Modifier,
         editModeStart: () -> Unit,
     ) {
+        val viewModel =
+            rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") {
+                viewModelFactory.create()
+            }
+
         DisposableEffect(tiles) {
             val token = Any()
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        val columns by viewModel.columns.collectAsStateWithLifecycle()
-        val rows by viewModel.rows.collectAsStateWithLifecycle()
+        val columns by viewModel.columns
+        val rows = viewModel.rows
 
         val pages =
             remember(tiles, columns, rows) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index 99a6cda..ca28ab3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -41,7 +41,8 @@
     viewModel: QuickQuickSettingsViewModel,
     modifier: Modifier = Modifier,
 ) {
-    val sizedTiles by viewModel.tileViewModels.collectAsStateWithLifecycle()
+
+    val sizedTiles = viewModel.tileViewModels
     val tiles = sizedTiles.fastMap { it.tile }
     val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
     val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
@@ -52,7 +53,7 @@
         tiles.forEach { it.startListening(token) }
         onDispose { tiles.forEach { it.stopListening(token) } }
     }
-    val columns by viewModel.columns.collectAsStateWithLifecycle()
+    val columns = viewModel.columns
     var cellIndex = 0
     Box(modifier = modifier) {
         GridAnchor()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 978a353..d107222 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -18,12 +18,12 @@
 
 import android.graphics.drawable.Animatable
 import android.text.TextUtils
+import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
 import androidx.compose.animation.graphics.res.animatedVectorResource
 import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
 import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
 import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -32,8 +32,9 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -44,6 +45,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.Shape
@@ -57,7 +59,9 @@
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.toggleableState
 import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.size
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
@@ -88,12 +92,14 @@
     ) {
         // Icon
         val longPressLabel = longPressLabel().takeIf { onLongClick != null }
+        val animatedBackgroundColor by
+            animateColorAsState(colors.iconBackground, label = "QSTileDualTargetBackgroundColor")
         Box(
             modifier =
                 Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) {
                     Modifier.clip(iconShape)
                         .verticalSquish(squishiness)
-                        .background(colors.iconBackground)
+                        .drawBehind { drawRect(animatedBackgroundColor) }
                         .combinedClickable(
                             onClick = toggleClick!!,
                             onLongClick = onLongClick,
@@ -117,6 +123,7 @@
             SmallTileContent(
                 icon = icon,
                 color = colors.icon,
+                size = { CommonTileDefaults.LargeTileIconSize },
                 modifier = Modifier.align(Alignment.Center),
             )
         }
@@ -139,18 +146,22 @@
     modifier: Modifier = Modifier,
     accessibilityUiState: AccessibilityUiState? = null,
 ) {
+    val animatedLabelColor by animateColorAsState(colors.label, label = "QSTileLabelColor")
+    val animatedSecondaryLabelColor by
+        animateColorAsState(colors.secondaryLabel, label = "QSTileSecondaryLabelColor")
     Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
-        Text(
+        BasicText(
             label,
             style = MaterialTheme.typography.labelLarge,
-            color = colors.label,
+            color = { animatedLabelColor },
             maxLines = 1,
             overflow = TextOverflow.Ellipsis,
         )
         if (!TextUtils.isEmpty(secondaryLabel)) {
-            Text(
+            BasicText(
                 secondaryLabel ?: "",
-                color = colors.secondaryLabel,
+                color = { animatedSecondaryLabelColor },
+                maxLines = 1,
                 style = MaterialTheme.typography.bodyMedium,
                 modifier =
                     Modifier.thenIf(
@@ -170,9 +181,11 @@
     modifier: Modifier = Modifier,
     icon: Icon,
     color: Color,
+    size: () -> Dp = { CommonTileDefaults.IconSize },
     animateToEnd: Boolean = false,
 ) {
-    val iconModifier = modifier.size(CommonTileDefaults.IconSize)
+    val animatedColor by animateColorAsState(color, label = "QSTileIconColor")
+    val iconModifier = modifier.size({ size().roundToPx() }, { size().roundToPx() })
     val context = LocalContext.current
     val loadedDrawable =
         remember(icon, context) {
@@ -182,7 +195,7 @@
             }
         }
     if (loadedDrawable !is Animatable) {
-        Icon(icon = icon, tint = color, modifier = iconModifier)
+        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
     } else if (icon is Icon.Resource) {
         val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
         val painter =
@@ -198,14 +211,15 @@
         Image(
             painter = painter,
             contentDescription = icon.contentDescription?.load(),
-            colorFilter = ColorFilter.tint(color = color),
+            colorFilter = ColorFilter.tint(color = animatedColor),
             modifier = iconModifier,
         )
     }
 }
 
 object CommonTileDefaults {
-    val IconSize = 24.dp
+    val IconSize = 32.dp
+    val LargeTileIconSize = 28.dp
     val ToggleTargetSize = 56.dp
     val TileHeight = 72.dp
     val TilePadding = 8.dp
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 418ed0b..b5cec12 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -20,6 +20,7 @@
 
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.fadeIn
@@ -54,7 +55,6 @@
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.ArrowBack
 import androidx.compose.material.icons.filled.Clear
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
@@ -69,21 +69,22 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.BiasAlignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInRoot
@@ -103,6 +104,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.util.fastMap
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.bounceable
 import com.android.compose.modifiers.height
 import com.android.systemui.common.ui.compose.load
@@ -134,9 +136,10 @@
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.model.groupAndSort
 import com.android.systemui.res.R
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.collectLatest
 
 object TileType
 
@@ -222,7 +225,7 @@
                         if (dragIsInProgress) {
                             RemoveTileTarget()
                         } else {
-                            Text(text = "Hold and drag to rearrange tiles.")
+                            Text(text = stringResource(id = R.string.drag_to_rearrange_tiles))
                         }
                     }
                 }
@@ -240,7 +243,9 @@
                             spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
                         modifier = modifier.fillMaxSize(),
                     ) {
-                        EditGridHeader { Text(text = "Hold and drag to add tiles.") }
+                        EditGridHeader {
+                            Text(text = stringResource(id = R.string.drag_to_add_tiles))
+                        }
 
                         AvailableTileGrid(otherTiles, selectionState, columns, listState)
                     }
@@ -286,7 +291,7 @@
                 .padding(10.dp),
     ) {
         Icon(imageVector = Icons.Default.Clear, contentDescription = null)
-        Text(text = "Remove")
+        Text(text = stringResource(id = R.string.qs_customize_remove))
     }
 }
 
@@ -409,7 +414,7 @@
 /**
  * Adds a list of [GridCell] to the lazy grid
  *
- * @param cells the pairs of [GridCell] to [BounceableTileViewModel]
+ * @param cells the pairs of [GridCell] to [AnimatableTileViewModel]
  * @param dragAndDropState the [DragAndDropState] for this grid
  * @param selectionState the [MutableSelectionState] for this grid
  * @param onToggleSize the callback when a tile's size is toggled
@@ -545,9 +550,27 @@
                     selectionState::unSelect,
                 )
                 .tileBackground(colors.background)
-                .tilePadding()
         ) {
-            EditTile(tile = cell.tile, iconOnly = cell.isIcon)
+            val targetValue = if (cell.isIcon) 0f else 1f
+            val animatedProgress = remember { Animatable(targetValue) }
+
+            if (selected) {
+                val resizingState = selectionState.resizingState
+                LaunchedEffect(targetValue, resizingState) {
+                    if (resizingState == null) {
+                        animatedProgress.animateTo(targetValue)
+                    } else {
+                        snapshotFlow { resizingState.progression }
+                            .collectLatest { animatedProgress.snapTo(it) }
+                    }
+                }
+            }
+
+            EditTile(
+                tile = cell.tile,
+                tileWidths = { tileWidths },
+                progress = { animatedProgress.value },
+            )
         }
     }
 }
@@ -612,45 +635,72 @@
 }
 
 @Composable
-fun BoxScope.EditTile(
+fun EditTile(
     tile: EditTileViewModel,
-    iconOnly: Boolean,
+    tileWidths: () -> TileWidths?,
+    progress: () -> Float,
     colors: TileColors = EditModeTileDefaults.editTileColors(),
 ) {
-    // Animated horizontal alignment from center (0f) to start (-1f)
-    val alignmentValue by
-        animateFloatAsState(
-            targetValue = if (iconOnly) 0f else -1f,
-            label = "QSEditTileContentAlignment",
-        )
-    val alignment by remember {
-        derivedStateOf { BiasAlignment(horizontalBias = alignmentValue, verticalBias = 0f) }
-    }
-    // Icon
-    Box(Modifier.size(ToggleTargetSize).align(alignment)) {
-        SmallTileContent(
-            icon = tile.icon,
-            color = colors.icon,
-            animateToEnd = true,
-            modifier = Modifier.align(Alignment.Center),
-        )
-    }
+    val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize
+    Row(
+        horizontalArrangement = spacedBy(6.dp),
+        verticalAlignment = Alignment.CenterVertically,
+        modifier =
+            Modifier.layout { measurable, constraints ->
+                    // Always display the tile using the large size and trust the parent composable
+                    // to clip the content as needed. This stop the labels from being truncated.
+                    val width = tileWidths()?.max ?: constraints.maxWidth
+                    val placeable =
+                        measurable.measure(constraints.copy(minWidth = width, maxWidth = width))
+                    val currentProgress = progress()
+                    val startPadding =
+                        if (currentProgress == 0f) {
+                            // Find the center of the max width when the tile is icon only
+                            iconHorizontalCenter(constraints.maxWidth)
+                        } else {
+                            // Find the center of the minimum width to hold the same position as the
+                            // tile is resized.
+                            val basePadding =
+                                tileWidths()?.min?.let { iconHorizontalCenter(it) } ?: 0f
+                            // Large tiles, represented with a progress of 1f, have a 0.dp padding
+                            basePadding * (1f - currentProgress)
+                        }
 
-    // Labels, positioned after the icon
-    AnimatedVisibility(visible = !iconOnly, enter = fadeIn(), exit = fadeOut()) {
+                    layout(constraints.maxWidth, constraints.maxHeight) {
+                        placeable.place(startPadding.roundToInt(), 0)
+                    }
+                }
+                .tilePadding(),
+    ) {
+        // Icon
+        Box(Modifier.size(ToggleTargetSize)) {
+            SmallTileContent(
+                icon = tile.icon,
+                color = colors.icon,
+                animateToEnd = true,
+                size = { CommonTileDefaults.IconSize - iconSizeDiff * progress() },
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+
+        // Labels, positioned after the icon
         LargeTileLabels(
             label = tile.label.text,
             secondaryLabel = tile.appName?.text,
             colors = colors,
-            modifier = Modifier.padding(start = ToggleTargetSize + TileArrangementPadding),
+            modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() },
         )
     }
 }
 
+private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float {
+    return (containerSize - ToggleTargetSize.roundToPx()) / 2f -
+        CommonTileDefaults.TilePadding.toPx()
+}
+
 private fun Modifier.tileBackground(color: Color): Modifier {
-    return drawBehind {
-        drawRoundRect(SolidColor(color), cornerRadius = CornerRadius(InactiveCornerRadius.toPx()))
-    }
+    // Clip tile contents from overflowing past the tile
+    return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) }
 }
 
 private object EditModeTileDefaults {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 91f2da2..19ab29e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -72,7 +72,7 @@
             rememberViewModel(traceName = "InfiniteGridLayout.TileGrid") {
                 viewModel.dynamicIconTilesViewModelFactory.create()
             }
-        val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle()
+        val columns by viewModel.gridSizeViewModel.columns
         val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
         val bounceables =
             remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
@@ -118,7 +118,7 @@
             rememberViewModel(traceName = "InfiniteGridLayout.EditTileGrid") {
                 viewModel.dynamicIconTilesViewModelFactory.create()
             }
-        val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle()
+        val columns by viewModel.gridSizeViewModel.columns
         val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle()
 
         // Non-current tiles should always be displayed as icon tiles.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index e1583e3..5bebdbc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -21,6 +21,7 @@
 import android.content.res.Resources
 import android.service.quicksettings.Tile.STATE_ACTIVE
 import android.service.quicksettings.Tile.STATE_INACTIVE
+import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.combinedClickable
@@ -61,6 +62,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.Expandable
 import com.android.compose.animation.bounceable
 import com.android.compose.modifiers.thenIf
@@ -74,6 +76,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.panels.ui.compose.BounceableInfo
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
 import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -82,7 +85,6 @@
 import com.android.systemui.res.R
 import java.util.function.Supplier
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 private const val TEST_TAG_SMALL = "qs_tile_small"
 private const val TEST_TAG_LARGE = "qs_tile_large"
@@ -128,14 +130,18 @@
 
     // TODO(b/361789146): Draw the shapes instead of clipping
     val tileShape = TileDefaults.animateTileShape(uiState.state)
-
-    TileExpandable(
-        color =
+    val animatedColor by
+        animateColorAsState(
             if (iconOnly || !uiState.handlesSecondaryClick) {
                 colors.iconBackground
             } else {
                 colors.background
             },
+            label = "QSTileBackgroundColor",
+        )
+
+    TileExpandable(
+        color = { animatedColor },
         shape = tileShape,
         squishiness = squishiness,
         hapticsViewModel = hapticsViewModel,
@@ -212,7 +218,7 @@
 
 @Composable
 private fun TileExpandable(
-    color: Color,
+    color: () -> Color,
     shape: Shape,
     squishiness: () -> Float,
     hapticsViewModel: TileHapticsViewModel?,
@@ -220,7 +226,7 @@
     content: @Composable (Expandable) -> Unit,
 ) {
     Expandable(
-        color = color,
+        color = color(),
         shape = shape,
         modifier = modifier.clip(shape).verticalSquish(squishiness),
     ) {
@@ -238,7 +244,7 @@
 ) {
     Box(
         modifier =
-            Modifier.height(CommonTileDefaults.TileHeight)
+            Modifier.height(TileHeight)
                 .fillMaxWidth()
                 .tileCombinedClickable(
                     onClick = onClick,
@@ -336,6 +342,16 @@
         )
 
     @Composable
+    fun inactiveDualTargetTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.surfaceVariant,
+            iconBackground = MaterialTheme.colorScheme.surfaceContainerHighest,
+            label = MaterialTheme.colorScheme.onSurfaceVariant,
+            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+            icon = MaterialTheme.colorScheme.onSurfaceVariant,
+        )
+
+    @Composable
     fun inactiveTileColors(): TileColors =
         TileColors(
             background = MaterialTheme.colorScheme.surfaceVariant,
@@ -365,7 +381,13 @@
                     activeTileColors()
                 }
             }
-            STATE_INACTIVE -> inactiveTileColors()
+            STATE_INACTIVE -> {
+                if (uiState.handlesSecondaryClick) {
+                    inactiveDualTargetTileColors()
+                } else {
+                    inactiveTileColors()
+                }
+            }
             else -> unavailableTileColors()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
index a084bc2..9552aa9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
@@ -17,25 +17,30 @@
 package com.android.systemui.qs.panels.ui.compose.selection
 
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.setValue
 import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD
 
 class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) {
-    // Total drag offset of this resize operation
-    private var totalOffset = 0f
+    /** Total drag offset of this resize operation. */
+    private var totalOffset by mutableFloatStateOf(0f)
 
     /** Width in pixels of the resizing tile. */
     var width by mutableIntStateOf(widths.base)
 
+    /** Progression between icon (0) and large (1) sizes. */
+    val progression
+        get() = calculateProgression()
+
     // Whether the tile is currently over the threshold and should be a large tile
-    private var passedThreshold: Boolean = passedThreshold(calculateProgression(width))
+    private var passedThreshold: Boolean = passedThreshold(progression)
 
     fun onDrag(offset: Float) {
         totalOffset += offset
         width = (widths.base + totalOffset).toInt().coerceIn(widths.min, widths.max)
 
-        passedThreshold(calculateProgression(width)).let {
+        passedThreshold(progression).let {
             // Resize if we went over the threshold
             if (passedThreshold != it) {
                 passedThreshold = it
@@ -49,7 +54,7 @@
     }
 
     /** The progression of the resizing tile between an icon tile (0f) and a large tile (1f) */
-    private fun calculateProgression(width: Int): Float {
+    private fun calculateProgression(): Float {
         return ((width - widths.min) / (widths.max - widths.min).toFloat()).coerceIn(0f, 1f)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 9f13a37..8a345ce 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -16,7 +16,11 @@
 
 package com.android.systemui.qs.panels.ui.compose.selection
 
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.spring
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.gestures.detectHorizontalDragGestures
 import androidx.compose.foundation.layout.Box
@@ -78,7 +82,6 @@
         ResizingHandle(
             enabled = selected,
             selectionState = selectionState,
-            transition = selectionAlpha,
             tileWidths = tileWidths,
             modifier =
                 // Higher zIndex to make sure the handle is drawn above the content
@@ -91,7 +94,6 @@
 private fun ResizingHandle(
     enabled: Boolean,
     selectionState: MutableSelectionState,
-    transition: () -> Float,
     tileWidths: () -> TileWidths?,
     modifier: Modifier = Modifier,
 ) {
@@ -126,19 +128,24 @@
                     }
             }
     ) {
-        ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center))
+        ResizingDot(enabled = enabled, modifier = Modifier.align(Alignment.Center))
     }
 }
 
 @Composable
 private fun ResizingDot(
-    transition: () -> Float,
+    enabled: Boolean,
     modifier: Modifier = Modifier,
     color: Color = MaterialTheme.colorScheme.primary,
 ) {
+    val alpha by animateFloatAsState(if (enabled) 1f else 0f)
+    val radius by
+        animateDpAsState(
+            if (enabled) ResizingDotSize / 2 else 0.dp,
+            animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
+        )
     Canvas(modifier = modifier.size(ResizingDotSize)) {
-        val v = transition()
-        drawCircle(color = color, radius = (ResizingDotSize / 2).toPx() * v, alpha = v)
+        drawCircle(color = color, radius = radius.toPx(), alpha = alpha)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt
index 03fc425..cbece2c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt
@@ -23,7 +23,7 @@
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.PlatformButton
-import com.android.compose.PlatformOutlinedButton
+import com.android.compose.PlatformTextButton
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dialog.ui.composable.AlertDialogContent
 import com.android.systemui.qs.panels.domain.interactor.EditTilesResetInteractor
@@ -84,8 +84,8 @@
                     Text(stringResource(id = android.R.string.ok))
                 }
             },
-            neutralButton = {
-                PlatformOutlinedButton(onClick = { dialog.dismiss() }) {
+            negativeButton = {
+                PlatformTextButton(onClick = { dialog.dismiss() }) {
                     Text(stringResource(id = android.R.string.cancel))
                 }
             },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModel.kt
new file mode 100644
index 0000000..225f542
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/*
+ * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page.
+ * This requires it to be a `@SysUISingleton` to be shared between viewmodels.
+ */
+@SysUISingleton
+class InFirstPageViewModel @Inject constructor() {
+    var inFirstPage = true
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt
index 0d12067..d687100 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.qs.panels.ui.dialog.QSResetDialogDelegate
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -27,12 +28,16 @@
     val gridSizeViewModel: QSColumnsViewModel,
     val squishinessViewModel: TileSquishinessViewModel,
     private val resetDialogDelegate: QSResetDialogDelegate,
-) {
+) : ExclusiveActivatable() {
 
     fun showResetDialog() {
         resetDialogDelegate.showDialog()
     }
 
+    override suspend fun onActivated(): Nothing {
+        gridSizeViewModel.activate()
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): InfiniteGridViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
index 0f7dafc..8bd9ed0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -16,33 +16,50 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.qs.panels.domain.interactor.PaginatedGridInteractor
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.stateIn
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 
-@SysUISingleton
 class PaginatedGridViewModel
-@Inject
+@AssistedInject
 constructor(
     iconTilesViewModel: IconTilesViewModel,
-    gridSizeViewModel: QSColumnsViewModel,
+    private val gridSizeViewModel: QSColumnsViewModel,
     paginatedGridInteractor: PaginatedGridInteractor,
-    @Application applicationScope: CoroutineScope,
-) : IconTilesViewModel by iconTilesViewModel, QSColumnsViewModel by gridSizeViewModel {
-    val rows =
-        paginatedGridInteractor.rows.stateIn(
-            applicationScope,
-            SharingStarted.WhileSubscribed(),
-            paginatedGridInteractor.defaultRows,
+    inFirstPageViewModel: InFirstPageViewModel,
+) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() {
+
+    private val hydrator = Hydrator("PaginatedGridViewModel")
+
+    val rows by
+        hydrator.hydratedStateOf(
+            traceName = "rows",
+            initialValue = paginatedGridInteractor.defaultRows,
+            source = paginatedGridInteractor.rows,
         )
 
-    /*
-     * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page.
-     * This requires it to be a `@SysUISingleton` to be shared between viewmodels.
-     */
-    var inFirstPage = true
+    var inFirstPage by inFirstPageViewModel::inFirstPage
+
+    val columns: State<Int>
+        get() = gridSizeViewModel.columns
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch { hydrator.activate() }
+            launch { gridSizeViewModel.activate() }
+            awaitCancellation()
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): PaginatedGridViewModel
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
index 0f1c77e..8926d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt
@@ -16,17 +16,25 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
-import com.android.systemui.dagger.SysUISingleton
+import androidx.compose.runtime.State
+import com.android.systemui.lifecycle.Activatable
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.qs.panels.domain.interactor.QSColumnsInteractor
 import javax.inject.Inject
-import kotlinx.coroutines.flow.StateFlow
 
-interface QSColumnsViewModel {
-    val columns: StateFlow<Int>
+interface QSColumnsViewModel : Activatable {
+    val columns: State<Int>
 }
 
-@SysUISingleton
 class QSColumnsSizeViewModelImpl @Inject constructor(interactor: QSColumnsInteractor) :
-    QSColumnsViewModel {
-    override val columns: StateFlow<Int> = interactor.columns
+    QSColumnsViewModel, ExclusiveActivatable() {
+    private val hydrator = Hydrator("QSColumnsSizeViewModelImpl")
+
+    override val columns =
+        hydrator.hydratedStateOf(traceName = "columns", source = interactor.columns)
+
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index 887a70f..0859c86 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -16,67 +16,69 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
 import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.qs.panels.domain.interactor.QuickQuickSettingsRowInteractor
-import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.shared.model.splitInRowsSequence
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
 class QuickQuickSettingsViewModel
-@Inject
+@AssistedInject
 constructor(
     tilesInteractor: CurrentTilesInteractor,
-    qsColumnsViewModel: QSColumnsViewModel,
+    private val qsColumnsViewModel: QSColumnsViewModel,
     quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor,
     val squishinessViewModel: TileSquishinessViewModel,
-    private val iconTilesViewModel: IconTilesViewModel,
-    @Application private val applicationScope: CoroutineScope,
+    iconTilesViewModel: IconTilesViewModel,
     val tileHapticsViewModelFactoryProvider: TileHapticsViewModelFactoryProvider,
-) {
+) : ExclusiveActivatable() {
 
-    val columns = qsColumnsViewModel.columns
+    private val hydrator = Hydrator("QuickQuickSettingsViewModel")
 
-    private val rows =
-        quickQuickSettingsRowInteractor.rows.stateIn(
-            applicationScope,
-            SharingStarted.WhileSubscribed(),
-            quickQuickSettingsRowInteractor.defaultRows,
+    val columns by qsColumnsViewModel.columns
+
+    private val largeTiles by
+        hydrator.hydratedStateOf(traceName = "largeTiles", source = iconTilesViewModel.largeTiles)
+
+    private val rows by
+        hydrator.hydratedStateOf(
+            traceName = "rows",
+            initialValue = quickQuickSettingsRowInteractor.defaultRows,
+            source = quickQuickSettingsRowInteractor.rows,
         )
 
-    val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> =
-        columns
-            .flatMapLatest { columns ->
-                tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) ->
-                    tiles
-                        .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
-                        .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
-                }
-            }
-            .stateIn(
-                applicationScope,
-                SharingStarted.WhileSubscribed(),
-                tilesInteractor.currentTiles.value
-                    .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
-                    .let {
-                        splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten()
-                    },
-            )
+    private val currentTiles by
+        hydrator.hydratedStateOf(traceName = "currentTiles", source = tilesInteractor.currentTiles)
+
+    val tileViewModels by derivedStateOf {
+        currentTiles
+            .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
+            .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
+    }
 
     private val TileSpec.width: Int
-        get() = if (iconTilesViewModel.isIconTile(this)) 1 else 2
+        get() = if (largeTiles.contains(this)) 2 else 1
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch { hydrator.activate() }
+            launch { qsColumnsViewModel.activate() }
+            awaitCancellation()
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): QuickQuickSettingsViewModel
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
index b1eb3bb3..da175c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
@@ -24,22 +24,31 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 
 class QuickSettingsContainerViewModel
 @AssistedInject
 constructor(
     brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory,
+    quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory,
     @Assisted supportsBrightnessMirroring: Boolean,
     val tileGridViewModel: TileGridViewModel,
     val editModeViewModel: EditModeViewModel,
-    val quickQuickSettingsViewModel: QuickQuickSettingsViewModel,
 ) : ExclusiveActivatable() {
 
     val brightnessSliderViewModel =
         brightnessSliderViewModelFactory.create(supportsBrightnessMirroring)
 
+    val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create()
+
     override suspend fun onActivated(): Nothing {
-        brightnessSliderViewModel.activate()
+        coroutineScope {
+            launch { brightnessSliderViewModel.activate() }
+            launch { quickQuickSettingsViewModel.activate() }
+            awaitCancellation()
+        }
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
index fc61e90..1d81e40 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
@@ -18,25 +18,33 @@
 
 import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 
+import dagger.Lazy;
+
 import javax.inject.Inject;
 
+
 /** Handles launching camera from Shade. */
 @SysUISingleton
 public class CameraLauncher {
     private final CameraGestureHelper mCameraGestureHelper;
     private final KeyguardBypassController mKeyguardBypassController;
+    private final Lazy<KeyguardQuickAffordanceInteractor> mKeyguardQuickAffordanceInteractorLazy;
 
     private boolean mLaunchingAffordance;
 
     @Inject
     public CameraLauncher(
             CameraGestureHelper cameraGestureHelper,
-            KeyguardBypassController keyguardBypassController
+            KeyguardBypassController keyguardBypassController,
+            Lazy<KeyguardQuickAffordanceInteractor> keyguardQuickAffordanceInteractorLazy
     ) {
         mCameraGestureHelper = cameraGestureHelper;
         mKeyguardBypassController = keyguardBypassController;
+        mKeyguardQuickAffordanceInteractorLazy = keyguardQuickAffordanceInteractorLazy;
     }
 
     /** Launches the camera. */
@@ -54,7 +62,12 @@
      */
     public void setLaunchingAffordance(boolean launchingAffordance) {
         mLaunchingAffordance = launchingAffordance;
-        mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
+        if (SceneContainerFlag.isEnabled()) {
+            mKeyguardQuickAffordanceInteractorLazy.get()
+                    .setLaunchingAffordance(launchingAffordance);
+        } else {
+            mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
index 5db1dcb..f741b85 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.smartspace.config
 
-import com.android.systemui.Flags.smartspaceSwipeEventLogging
+import com.android.systemui.Flags.smartspaceSwipeEventLoggingFix
 import com.android.systemui.Flags.smartspaceViewpager2
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
@@ -30,5 +30,5 @@
         get() = smartspaceViewpager2()
 
     override val isSwipeEventLoggingEnabled: Boolean
-        get() = smartspaceSwipeEventLogging()
+        get() = smartspaceSwipeEventLoggingFix()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index ea515e0..08ffbf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -22,11 +22,13 @@
 import androidx.core.animation.ObjectAnimator
 import com.android.app.animation.Interpolators
 import com.android.app.animation.InterpolatorsAndroidX
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Dumpable
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
@@ -49,7 +51,6 @@
 import kotlin.math.max
 import kotlin.math.min
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 @SysUISingleton
 class NotificationWakeUpCoordinator
@@ -65,6 +66,7 @@
     private val logger: NotificationWakeUpCoordinatorLogger,
     private val notifsKeyguardInteractor: NotificationsKeyguardInteractor,
     private val communalInteractor: CommunalInteractor,
+    private val pulseExpansionInteractor: PulseExpansionInteractor,
 ) :
     OnHeadsUpChangedListener,
     StatusBarStateController.StateListener,
@@ -115,7 +117,7 @@
                     // they were blocked by the proximity sensor
                     updateNotificationVisibility(
                         animate = shouldAnimateVisibility(),
-                        increaseSpeed = false
+                        increaseSpeed = false,
                     )
                 }
             }
@@ -139,7 +141,7 @@
                 // the waking up callback
                 updateNotificationVisibility(
                     animate = shouldAnimateVisibility(),
-                    increaseSpeed = false
+                    increaseSpeed = false,
                 )
             }
         }
@@ -200,7 +202,7 @@
                         setNotificationsVisibleForExpansion(
                             visible = false,
                             animate = false,
-                            increaseSpeed = false
+                            increaseSpeed = false,
                         )
                     }
                 }
@@ -226,7 +228,7 @@
                 for (listener in wakeUpListeners) {
                     listener.onPulseExpandingChanged(pulseExpanding)
                 }
-                notifsKeyguardInteractor.setPulseExpanding(pulseExpanding)
+                pulseExpansionInteractor.setPulseExpanding(pulseExpanding)
             }
         }
     }
@@ -241,7 +243,7 @@
     fun setNotificationsVisibleForExpansion(
         visible: Boolean,
         animate: Boolean,
-        increaseSpeed: Boolean
+        increaseSpeed: Boolean,
     ) {
         notificationsVisibleForExpansion = visible
         updateNotificationVisibility(animate, increaseSpeed)
@@ -282,7 +284,7 @@
     private fun setNotificationsVisible(
         visible: Boolean,
         animate: Boolean,
-        increaseSpeed: Boolean
+        increaseSpeed: Boolean,
     ) {
         if (notificationsVisible == visible) {
             return
@@ -363,7 +365,7 @@
             hardOverride = hardDozeAmountOverride,
             outputLinear = outputLinearDozeAmount,
             state = statusBarStateController.state,
-            changed = changed
+            changed = changed,
         )
         stackScrollerController.setDozeAmount(outputEasedDozeAmount)
         updateHideAmount()
@@ -372,7 +374,7 @@
             setNotificationsVisibleForExpansion(
                 visible = false,
                 animate = false,
-                increaseSpeed = false
+                increaseSpeed = false,
             )
         }
     }
@@ -389,10 +391,7 @@
      * call with `false` at some point in the near future. A call with `true` before that will
      * happen if the animation is not already running.
      */
-    fun setWakingUp(
-        wakingUp: Boolean,
-        requestDelayedAnimation: Boolean,
-    ) {
+    fun setWakingUp(wakingUp: Boolean, requestDelayedAnimation: Boolean) {
         logger.logSetWakingUp(wakingUp, requestDelayedAnimation)
         this.wakingUp = wakingUp
         if (wakingUp && requestDelayedAnimation) {
@@ -432,7 +431,7 @@
             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
             setHardDozeAmountOverride(
                 dozing = false,
-                source = "Override: Shade->Shade (lock cancelled by unlock)"
+                source = "Override: Shade->Shade (lock cancelled by unlock)",
             )
             this.state = newState
             return
@@ -478,7 +477,7 @@
                 wasCollapsedEnoughToHide,
                 isCollapsedEnoughToHide,
                 couldShowPulsingHuns,
-                canShowPulsingHuns
+                canShowPulsingHuns,
             )
 
             if (couldShowPulsingHuns && !canShowPulsingHuns) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
index bd6ea30..f9fd5ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
@@ -24,7 +24,4 @@
 class NotificationsKeyguardViewStateRepository @Inject constructor() {
     /** Are notifications fully hidden from view? */
     val areNotificationsFullyHidden = MutableStateFlow(false)
-
-    /** Is a pulse expansion occurring? */
-    val isPulseExpanding = MutableStateFlow(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
index a6361cb..1cb4144 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
@@ -22,12 +22,7 @@
 /** Domain logic pertaining to notifications on the keyguard. */
 class NotificationsKeyguardInteractor
 @Inject
-constructor(
-    private val repository: NotificationsKeyguardViewStateRepository,
-) {
-    /** Is a pulse expansion occurring? */
-    val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding
-
+constructor(private val repository: NotificationsKeyguardViewStateRepository) {
     /** Are notifications fully hidden from view? */
     val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden
 
@@ -35,9 +30,4 @@
     fun setNotificationsFullyHidden(fullyHidden: Boolean) {
         repository.areNotificationsFullyHidden.value = fullyHidden
     }
-
-    /** Updates whether a pulse expansion is occurring. */
-    fun setPulseExpanding(pulseExpanding: Boolean) {
-        repository.isPulseExpanding.value = pulseExpanding
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index c1d72e4..f02f7f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -678,7 +678,9 @@
         mGroupMembershipManager = Dependency.get(GroupMembershipManager.class);
         mGroupExpansionManager = Dependency.get(GroupExpansionManager.class);
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-        setWindowInsetsAnimationCallback(mInsetsCallback);
+        if (!SceneContainerFlag.isEnabled()) {
+            setWindowInsetsAnimationCallback(mInsetsCallback);
+        }
     }
 
     /**
@@ -1694,7 +1696,7 @@
             } else if (mQsFullScreen) {
                 int stackStartPosition =
                         getContentHeight() - getTopPadding() + getIntrinsicPadding();
-                int stackEndPosition = mMaxTopPadding + mShelf.getIntrinsicHeight();
+                int stackEndPosition = getMaxTopPadding() + mShelf.getIntrinsicHeight();
                 if (stackStartPosition <= stackEndPosition) {
                     stackHeight = stackEndPosition;
                 } else {
@@ -2086,6 +2088,7 @@
     }
 
     private void updateImeInset(WindowInsets windowInsets) {
+        SceneContainerFlag.assertInLegacyMode();
         mImeInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom;
 
         if (mFooterView != null && mFooterView.getViewState() != null) {
@@ -2112,7 +2115,7 @@
         if (cutout != null) {
             mWaterfallTopInset = cutout.getWaterfallInsets().top;
         }
-        if (!mIsInsetAnimationRunning) {
+        if (!SceneContainerFlag.isEnabled() && !mIsInsetAnimationRunning) {
             // update bottom inset e.g. after rotation
             updateImeInset(insets);
         }
@@ -2513,6 +2516,7 @@
     }
 
     private int getImeInset() {
+        SceneContainerFlag.assertInLegacyMode();
         // The NotificationStackScrollLayout does not extend all the way to the bottom of the
         // display. Therefore, subtract that space from the mImeInset, in order to only include
         // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout.
@@ -2851,11 +2855,6 @@
         setExpandedHeight(mExpandedHeight);
     }
 
-    public void setMaxTopPadding(int maxTopPadding) {
-        SceneContainerFlag.assertInLegacyMode();
-        mMaxTopPadding = maxTopPadding;
-    }
-
     public int getLayoutMinHeight() {
         SceneContainerFlag.assertInLegacyMode();
         return getLayoutMinHeightInternal();
@@ -3639,7 +3638,11 @@
      * @return Whether a y coordinate is inside the content.
      */
     public boolean isInContentBounds(float y) {
-        return y < getHeight() - getEmptyBottomMarginInternal();
+        if (SceneContainerFlag.isEnabled()) {
+            return y < mAmbientState.getStackCutoff();
+        } else {
+            return y < getHeight() - getEmptyBottomMarginInternal();
+        }
     }
 
     private float getTouchSlop(MotionEvent event) {
@@ -5510,7 +5513,6 @@
             println(pw, "alpha", getAlpha());
             println(pw, "suppressChildrenMeasureLayout", mSuppressChildrenMeasureAndLayout);
             println(pw, "scrollY", mAmbientState.getScrollY());
-            println(pw, "maxTopPadding", mMaxTopPadding);
             println(pw, "showShelfOnly", mShouldShowShelfOnly);
             println(pw, "qsExpandFraction", mQsExpansionFraction);
             println(pw, "isCurrentUserSetup", mIsCurrentUserSetup);
@@ -5547,6 +5549,7 @@
                 println(pw, "intrinsicContentHeight", getIntrinsicContentHeight());
                 println(pw, "contentHeight", getContentHeight());
                 println(pw, "topPadding", getTopPadding());
+                println(pw, "maxTopPadding", getMaxTopPadding());
             }
         });
         pw.println();
@@ -6953,10 +6956,12 @@
 
     /** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */
     private int getContentHeight() {
+        SceneContainerFlag.assertInLegacyMode();
         return mContentHeight;
     }
 
     private void setContentHeight(int contentHeight) {
+        SceneContainerFlag.assertInLegacyMode();
         mContentHeight = contentHeight;
     }
 
@@ -6965,10 +6970,23 @@
      * @return the height of the content ignoring the footer.
      */
     public float getIntrinsicContentHeight() {
+        SceneContainerFlag.assertInLegacyMode();
         return mIntrinsicContentHeight;
     }
 
     private void setIntrinsicContentHeight(float intrinsicContentHeight) {
+        SceneContainerFlag.assertInLegacyMode();
         mIntrinsicContentHeight = intrinsicContentHeight;
     }
+
+    private int getMaxTopPadding() {
+        SceneContainerFlag.assertInLegacyMode();
+        return mMaxTopPadding;
+    }
+
+    /** Not used with SceneContainerFlag, because we rely on the placeholder for placement. */
+    public void setMaxTopPadding(int maxTopPadding) {
+        SceneContainerFlag.assertInLegacyMode();
+        mMaxTopPadding = maxTopPadding;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
index ffab9ea..42acd7bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
@@ -29,19 +29,15 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.constraintlayout.widget.ConstraintSet.VERTICAL
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition
 
 /**
  * Container for the stack scroller, so that the bounds can be externally specified, such as from
  * the keyguard or shade scenes.
  */
-class SharedNotificationContainer(
-    context: Context,
-    private val attrs: AttributeSet?,
-) :
-    ConstraintLayout(
-        context,
-        attrs,
-    ) {
+class SharedNotificationContainer(context: Context, attrs: AttributeSet?) :
+    ConstraintLayout(context, attrs) {
 
     private val baseConstraintSet = ConstraintSet()
 
@@ -59,24 +55,40 @@
     }
 
     fun updateConstraints(
-        useSplitShade: Boolean,
+        horizontalPosition: HorizontalPosition,
         marginStart: Int,
         marginTop: Int,
         marginEnd: Int,
-        marginBottom: Int
+        marginBottom: Int,
     ) {
         val constraintSet = ConstraintSet()
         constraintSet.clone(baseConstraintSet)
 
         val startConstraintId =
-            if (useSplitShade) {
+            if (horizontalPosition is HorizontalPosition.MiddleToEdge) {
                 R.id.nssl_guideline
             } else {
                 PARENT_ID
             }
+
         val nsslId = R.id.notification_stack_scroller
         constraintSet.apply {
-            connect(nsslId, START, startConstraintId, START, marginStart)
+            if (SceneContainerFlag.isEnabled) {
+                when (horizontalPosition) {
+                    is HorizontalPosition.FloatAtEnd ->
+                        constrainWidth(nsslId, horizontalPosition.width)
+                    is HorizontalPosition.MiddleToEdge ->
+                        setGuidelinePercent(R.id.nssl_guideline, horizontalPosition.ratio)
+                    else -> Unit
+                }
+            }
+
+            if (
+                !SceneContainerFlag.isEnabled ||
+                    horizontalPosition !is HorizontalPosition.FloatAtEnd
+            ) {
+                connect(nsslId, START, startConstraintId, START, marginStart)
+            }
             connect(nsslId, END, PARENT_ID, END, marginEnd)
             connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom)
             connect(nsslId, TOP, PARENT_ID, TOP, marginTop)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index ce89d78..4a55dfa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -18,6 +18,7 @@
 
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags
 import com.android.systemui.common.ui.view.onLayoutChanged
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
@@ -36,11 +37,8 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Binds the shared notification container to its view-model. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class SharedNotificationContainerBinder
 @Inject
@@ -74,7 +72,7 @@
                     launch {
                         viewModel.configurationBasedDimensions.collect {
                             view.updateConstraints(
-                                useSplitShade = it.useSplitShade,
+                                horizontalPosition = it.horizontalPosition,
                                 marginStart = it.marginStart,
                                 marginTop = it.marginTop,
                                 marginEnd = it.marginEnd,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index e6663d5..96d0d76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -224,33 +224,54 @@
         if (SceneContainerFlag.isEnabled) {
                 combine(
                     shadeInteractor.isShadeLayoutWide,
+                    shadeInteractor.shadeMode,
                     configurationInteractor.onAnyConfigurationChange,
-                ) { isShadeLayoutWide, _ ->
+                ) { isShadeLayoutWide, shadeMode, _ ->
                     with(context.resources) {
                         // TODO(b/338033836): Define separate horizontal margins for dual shade.
                         val marginHorizontal =
                             getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
+
+                        val horizontalPosition =
+                            when (shadeMode) {
+                                Single -> HorizontalPosition.EdgeToEdge
+                                Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f)
+                                Dual ->
+                                    if (isShadeLayoutWide) {
+                                        HorizontalPosition.FloatAtEnd(
+                                            width = getDimensionPixelSize(R.dimen.shade_panel_width)
+                                        )
+                                    } else {
+                                        HorizontalPosition.EdgeToEdge
+                                    }
+                            }
+
                         ConfigurationBasedDimensions(
-                            marginStart = if (isShadeLayoutWide) 0 else marginHorizontal,
+                            horizontalPosition = horizontalPosition,
+                            marginStart =
+                                if (horizontalPosition is HorizontalPosition.EdgeToEdge)
+                                    marginHorizontal
+                                else 0,
                             marginEnd = marginHorizontal,
                             marginBottom =
                                 getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
                             // y position of the NSSL in the window needs to be 0 under scene
                             // container
                             marginTop = 0,
-                            useSplitShade = isShadeLayoutWide,
                         )
                     }
                 }
             } else {
                 interactor.configurationBasedDimensions.map {
                     ConfigurationBasedDimensions(
+                        horizontalPosition =
+                            if (it.useSplitShade) HorizontalPosition.MiddleToEdge()
+                            else HorizontalPosition.EdgeToEdge,
                         marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
                         marginEnd = it.marginHorizontal,
                         marginBottom = it.marginBottom,
                         marginTop =
                             if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
-                        useSplitShade = it.useSplitShade,
                     )
                 }
             }
@@ -446,59 +467,63 @@
      */
     private val alphaForShadeAndQsExpansion: Flow<Float> =
         if (SceneContainerFlag.isEnabled) {
-            shadeInteractor.shadeMode.flatMapLatest { shadeMode ->
-                when (shadeMode) {
-                    Single ->
-                        combineTransform(
-                            shadeInteractor.shadeExpansion,
-                            shadeInteractor.qsExpansion,
-                        ) { shadeExpansion, qsExpansion ->
-                            if (qsExpansion == 1f) {
-                                // Ensure HUNs will be visible in QS shade (at least while unlocked)
+                shadeInteractor.shadeMode.flatMapLatest { shadeMode ->
+                    when (shadeMode) {
+                        Single ->
+                            combineTransform(
+                                shadeInteractor.shadeExpansion,
+                                shadeInteractor.qsExpansion,
+                            ) { shadeExpansion, qsExpansion ->
+                                if (qsExpansion == 1f) {
+                                    // Ensure HUNs will be visible in QS shade (at least while
+                                    // unlocked)
+                                    emit(1f)
+                                } else if (shadeExpansion > 0f || qsExpansion > 0f) {
+                                    // Fade as QS shade expands
+                                    emit(1f - qsExpansion)
+                                }
+                            }
+                        Split -> isAnyExpanded.filter { it }.map { 1f }
+                        Dual ->
+                            combineTransform(
+                                headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
+                                shadeInteractor.shadeExpansion,
+                                shadeInteractor.qsExpansion,
+                            ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
+                                if (isHeadsUpOrAnimatingAway) {
+                                    // Ensure HUNs will be visible in QS shade (at least while
+                                    // unlocked)
+                                    emit(1f)
+                                } else if (shadeExpansion > 0f || qsExpansion > 0f) {
+                                    // Fade out as QS shade expands
+                                    emit(1f - qsExpansion)
+                                }
+                            }
+                    }
+                }
+            } else {
+                interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions
+                    ->
+                    combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) {
+                        shadeExpansion,
+                        qsExpansion ->
+                        if (shadeExpansion > 0f || qsExpansion > 0f) {
+                            if (configurationBasedDimensions.useSplitShade) {
                                 emit(1f)
-                            } else if (shadeExpansion > 0f || qsExpansion > 0f) {
+                            } else if (qsExpansion == 1f) {
+                                // Ensure HUNs will be visible in QS shade (at least while
+                                // unlocked)
+                                emit(1f)
+                            } else {
                                 // Fade as QS shade expands
                                 emit(1f - qsExpansion)
                             }
                         }
-                    Split -> isAnyExpanded.filter { it }.map { 1f }
-                    Dual ->
-                        combineTransform(
-                            headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
-                            shadeInteractor.shadeExpansion,
-                            shadeInteractor.qsExpansion,
-                        ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
-                            if (isHeadsUpOrAnimatingAway) {
-                                // Ensure HUNs will be visible in QS shade (at least while unlocked)
-                                emit(1f)
-                            } else if (shadeExpansion > 0f || qsExpansion > 0f) {
-                                // Fade out as QS shade expands
-                                emit(1f - qsExpansion)
-                            }
-                        }
-                }
-            }
-        } else {
-            interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions ->
-                combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) {
-                    shadeExpansion,
-                    qsExpansion ->
-                    if (shadeExpansion > 0f || qsExpansion > 0f) {
-                        if (configurationBasedDimensions.useSplitShade) {
-                            emit(1f)
-                        } else if (qsExpansion == 1f) {
-                            // Ensure HUNs will be visible in QS shade (at least while unlocked)
-                            emit(1f)
-                        } else {
-                            // Fade as QS shade expands
-                            emit(1f - qsExpansion)
-                        }
                     }
                 }
             }
-        }
-        .onStart { emit(1f) }
-        .dumpWhileCollecting("alphaForShadeAndQsExpansion")
+            .onStart { emit(1f) }
+            .dumpWhileCollecting("alphaForShadeAndQsExpansion")
 
     val panelAlpha = keyguardInteractor.panelAlpha
 
@@ -766,10 +791,25 @@
     }
 
     data class ConfigurationBasedDimensions(
+        val horizontalPosition: HorizontalPosition,
         val marginStart: Int,
         val marginTop: Int,
         val marginEnd: Int,
         val marginBottom: Int,
-        val useSplitShade: Boolean,
     )
+
+    /** Specifies the horizontal layout constraints for the notification container. */
+    sealed interface HorizontalPosition {
+        /** The container is using the full width of the screen (minus any margins). */
+        data object EdgeToEdge : HorizontalPosition
+
+        /** The container is laid out from the given [ratio] of the screen width to the end edge. */
+        data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition
+
+        /**
+         * The container has a fixed [width] and is aligned to the end of the screen. In this
+         * layout, the start edge of the container is floating, i.e. unconstrained.
+         */
+        data class FloatAtEnd(val width: Int) : HorizontalPosition
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index a658115..d2c2003 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -37,6 +37,7 @@
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
@@ -78,6 +79,7 @@
     private View mNotificationShadeWindowView;
     private View mNotificationPanelView;
     private boolean mForceCollapsedUntilLayout = false;
+    private Boolean mCommunalVisible = false;
 
     private Region mTouchableRegion = new Region();
     private int mDisplayCutoutTouchableRegionSize;
@@ -98,7 +100,8 @@
             JavaAdapter javaAdapter,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             PrimaryBouncerInteractor primaryBouncerInteractor,
-            AlternateBouncerInteractor alternateBouncerInteractor
+            AlternateBouncerInteractor alternateBouncerInteractor,
+            CommunalSceneInteractor communalSceneInteractor
     ) {
         mContext = context;
         initResources();
@@ -145,6 +148,9 @@
             javaAdapter.alwaysCollectFlow(
                     shadeInteractor.isAnyExpanded(),
                     this::onShadeOrQsExpanded);
+            javaAdapter.alwaysCollectFlow(
+                    communalSceneInteractor.isCommunalVisible(),
+                    this::onCommunalVisible);
         }
 
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
@@ -196,6 +202,10 @@
         }
     }
 
+    private void onCommunalVisible(Boolean visible) {
+        mCommunalVisible = visible;
+    }
+
     /**
      * Calculates the touch region needed for heads up notifications, taking into consideration
      * any existing display cutouts (notch)
@@ -304,6 +314,9 @@
                 && (!mIsSceneContainerUiEmpty || mIsRemoteUserInteractionOngoing))
                 || mPrimaryBouncerInteractor.isShowing().getValue()
                 || mAlternateBouncerInteractor.isVisibleState()
+                // The glanceable hub is a full-screen UI within the notification shade window. When
+                // it's visible, the touchable region should be the full screen.
+                || mCommunalVisible
                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
index 5392e38..903c7e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -34,7 +34,7 @@
 
     LazyVerticalGrid(
         columns = GridCells.Fixed(1),
-        modifier = Modifier.fillMaxWidth().heightIn(max = 320.dp),
+        modifier = Modifier.fillMaxWidth().heightIn(max = 280.dp),
         verticalArrangement = Arrangement.spacedBy(8.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp),
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 61eeab3..e1b8a1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -22,7 +22,6 @@
 import android.hardware.biometrics.BiometricAuthenticator
 import android.hardware.biometrics.BiometricConstants
 import android.hardware.biometrics.BiometricManager
-import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT
 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
 import android.hardware.biometrics.PromptInfo
 import android.hardware.biometrics.PromptVerticalListContentView
@@ -138,6 +137,7 @@
         PromptSelectorInteractorImpl(
             fingerprintRepository,
             displayStateInteractor,
+            credentialInteractor,
             biometricPromptRepository,
             lockPatternUtils,
         )
@@ -412,7 +412,6 @@
 
     @Test
     fun testShowBiometricUI_ContentViewWithMoreOptionsButton() {
-        mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         var isButtonClicked = false
         val contentView =
             PromptContentViewWithMoreOptionsButton.Builder()
@@ -449,7 +448,6 @@
 
     @Test
     fun testShowCredentialUI_withVerticalListContentView() {
-        mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         val container =
             initializeFingerprintContainer(
                 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
@@ -470,7 +468,6 @@
 
     @Test
     fun testShowCredentialUI_withContentViewWithMoreOptionsButton() {
-        mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         val contentView =
             PromptContentViewWithMoreOptionsButton.Builder()
                 .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 06e8b1c..57fad83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -1207,6 +1207,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testWindowInsetAnimationProgress_updatesBottomInset() {
         int imeInset = 100;
         WindowInsets windowInsets = new WindowInsets.Builder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt
new file mode 100644
index 0000000..0c0b5ba
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt
@@ -0,0 +1,238 @@
+/*
+ * 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.statusbar.phone.data.repository
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.KeyguardBypassRepository
+import com.android.systemui.keyguard.data.repository.configureKeyguardBypass
+import com.android.systemui.keyguard.data.repository.keyguardBypassRepository
+import com.android.systemui.keyguard.data.repository.verifyCallback
+import com.android.systemui.keyguard.data.repository.verifyNoCallback
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.statusbar.policy.devicePostureController
+import com.android.systemui.testKosmos
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.tuner.tunerService
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class KeyguardBypassRepositoryTest : SysuiTestCase() {
+    @JvmField @Rule val mockito: MockitoRule = MockitoJUnit.rule()
+
+    private lateinit var tunableCallback: TunerService.Tunable
+    private lateinit var postureControllerCallback: DevicePostureController.Callback
+
+    private val kosmos = testKosmos()
+    private lateinit var underTest: KeyguardBypassRepository
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    // overrideFaceBypassSetting overridden to true
+    // isFaceEnrolledAndEnabled true
+    // isPostureAllowedForFaceAuth true/false on posture changes
+    @Test
+    fun updatesBypassAvailableOnPostureChanges_bypassOverrideAlways() =
+        testScope.runTest {
+            // KeyguardBypassRepository#overrideFaceBypassSetting = true due to ALWAYS override
+            // Initialize face auth posture to DEVICE_POSTURE_OPENED config
+            initUnderTest(
+                faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_ALWAYS,
+                faceAuthPostureConfig = DEVICE_POSTURE_CLOSED,
+            )
+            val isBypassAvailable by collectLastValue(underTest.isBypassAvailable)
+            runCurrent()
+
+            postureControllerCallback = kosmos.devicePostureController.verifyCallback()
+
+            // Update face auth posture to match config
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+            // Assert bypass available
+            assertThat(isBypassAvailable).isTrue()
+
+            // Set face auth posture to not match config
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+            // Assert bypass not available
+            assertThat(isBypassAvailable).isFalse()
+        }
+
+    // overrideFaceBypassSetting overridden to false
+    // isFaceEnrolledAndEnabled true
+    // isPostureAllowedForFaceAuth true/false on posture changes
+    @Test
+    fun updatesBypassEnabledOnPostureChanges_bypassOverrideNever() =
+        testScope.runTest {
+            // KeyguardBypassRepository#overrideFaceBypassSetting = false due to NEVER override
+            // Initialize face auth posture to DEVICE_POSTURE_OPENED config
+            initUnderTest(
+                faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NEVER,
+                faceAuthPostureConfig = DEVICE_POSTURE_CLOSED,
+            )
+            val bypassEnabled by collectLastValue(underTest.isBypassAvailable)
+            runCurrent()
+            postureControllerCallback = kosmos.devicePostureController.verifyCallback()
+
+            // Update face auth posture to match config
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+            // Assert bypass not enabled
+            assertThat(bypassEnabled).isFalse()
+
+            // Set face auth posture to not match config
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+            // Assert bypass not enabled
+            assertThat(bypassEnabled).isFalse()
+        }
+
+    // overrideFaceBypassSetting set true/false depending on Setting
+    // isFaceEnrolledAndEnabled true
+    // isPostureAllowedForFaceAuth true
+    @Test
+    fun updatesBypassEnabledOnSettingsChanges_bypassNoOverride_devicePostureMatchesConfig() =
+        testScope.runTest {
+            // No bypass override
+            // Initialize face auth posture to DEVICE_POSTURE_OPENED config
+            initUnderTest(
+                faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NO_OVERRIDE,
+                faceAuthPostureConfig = DEVICE_POSTURE_CLOSED,
+            )
+
+            val bypassEnabled by collectLastValue(underTest.isBypassAvailable)
+            runCurrent()
+            postureControllerCallback = kosmos.devicePostureController.verifyCallback()
+            tunableCallback = kosmos.tunerService.captureCallback()
+
+            // Update face auth posture to match config
+            postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+            // FACE_UNLOCK_DISMISSES_KEYGUARD setting true
+            whenever(kosmos.tunerService.getValue(eq(faceUnlockDismissesKeyguard), anyInt()))
+                .thenReturn(1)
+            tunableCallback.onTuningChanged(faceUnlockDismissesKeyguard, "")
+
+            runCurrent()
+            // Assert bypass enabled
+            assertThat(bypassEnabled).isTrue()
+
+            // FACE_UNLOCK_DISMISSES_KEYGUARD setting false
+            whenever(kosmos.tunerService.getValue(eq(faceUnlockDismissesKeyguard), anyInt()))
+                .thenReturn(0)
+            tunableCallback.onTuningChanged(faceUnlockDismissesKeyguard, "")
+
+            runCurrent()
+            // Assert bypass not enabled
+            assertThat(bypassEnabled).isFalse()
+        }
+
+    // overrideFaceBypassSetting overridden to true
+    // isFaceEnrolledAndEnabled true
+    // isPostureAllowedForFaceAuth always true given DEVICE_POSTURE_UNKNOWN config
+    @Test
+    fun bypassEnabledTrue_bypassAlways_unknownDevicePostureConfig() =
+        testScope.runTest {
+            // KeyguardBypassRepository#overrideFaceBypassSetting = true due to ALWAYS override
+            // Set face auth posture config to unknown
+            initUnderTest(
+                faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_ALWAYS,
+                faceAuthPostureConfig = DEVICE_POSTURE_UNKNOWN,
+            )
+            val bypassEnabled by collectLastValue(underTest.isBypassAvailable)
+            kosmos.devicePostureController.verifyNoCallback()
+
+            // Assert bypass enabled
+            assertThat(bypassEnabled).isTrue()
+        }
+
+    // overrideFaceBypassSetting overridden to false
+    // isFaceEnrolledAndEnabled true
+    // isPostureAllowedForFaceAuth always true given DEVICE_POSTURE_UNKNOWN config
+    @Test
+    fun bypassEnabledFalse_bypassNever_unknownDevicePostureConfig() =
+        testScope.runTest {
+            // KeyguardBypassRepository#overrideFaceBypassSetting = false due to NEVER override
+            // Set face auth posture config to unknown
+            initUnderTest(
+                faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NEVER,
+                faceAuthPostureConfig = DEVICE_POSTURE_UNKNOWN,
+            )
+            val bypassEnabled by collectLastValue(underTest.isBypassAvailable)
+            kosmos.devicePostureController.verifyNoCallback()
+
+            // Assert bypass enabled
+            assertThat(bypassEnabled).isFalse()
+        }
+
+    private fun TestScope.initUnderTest(
+        faceUnlockBypassOverrideConfig: Int,
+        faceAuthPostureConfig: Int,
+    ) {
+        kosmos.configureKeyguardBypass(
+            faceAuthEnrolledAndEnabled = true,
+            faceUnlockBypassOverrideConfig = faceUnlockBypassOverrideConfig,
+            faceAuthPostureConfig = faceAuthPostureConfig,
+        )
+        underTest = kosmos.keyguardBypassRepository
+        runCurrent()
+    }
+
+    companion object {
+        private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0
+        private const val FACE_UNLOCK_BYPASS_ALWAYS = 1
+        private const val FACE_UNLOCK_BYPASS_NEVER = 2
+    }
+}
+
+private const val faceUnlockDismissesKeyguard = Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD
+
+private fun TunerService.captureCallback() =
+    withArgCaptor<TunerService.Tunable> {
+        verify(this@captureCallback).addTunable(capture(), eq(faceUnlockDismissesKeyguard))
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index 020f7fa..c945812 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -30,6 +30,8 @@
 import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.PulseExpansionRepository
 import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
 import com.android.systemui.scene.SceneContainerFrameworkModule
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -70,11 +72,17 @@
 interface SysUITestModule {
 
     @Binds fun bindTestableContext(sysuiTestableContext: SysuiTestableContext): TestableContext
+
     @Binds fun bindContext(testableContext: TestableContext): Context
+
     @Binds @Application fun bindAppContext(context: Context): Context
+
     @Binds @Application fun bindAppResources(resources: Resources): Resources
+
     @Binds @Main fun bindMainResources(resources: Resources): Resources
+
     @Binds fun bindBroadcastDispatcher(fake: FakeBroadcastDispatcher): BroadcastDispatcher
+
     @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor
 
     @Binds
@@ -108,7 +116,7 @@
         @Provides
         fun provideBaseShadeInteractor(
             sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
-            sceneContainerOff: Provider<ShadeInteractorLegacyImpl>
+            sceneContainerOff: Provider<ShadeInteractorLegacyImpl>,
         ): BaseShadeInteractor {
             return if (SceneContainerFlag.isEnabled) {
                 sceneContainerOn.get()
@@ -125,6 +133,12 @@
         ): SceneDataSourceDelegator {
             return SceneDataSourceDelegator(applicationScope, config)
         }
+
+        @Provides
+        @SysUISingleton
+        fun providesPulseExpansionRepository(dumpManager: DumpManager): PulseExpansionRepository {
+            return PulseExpansionRepository(dumpManager)
+        }
     }
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt
index 56297f0..787a471 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt
@@ -27,6 +27,7 @@
         fingerprintPropertyRepository = fingerprintPropertyRepository,
         displayStateInteractor = displayStateInteractor,
         promptRepository = promptRepository,
-        lockPatternUtils = lockPatternUtils
+        credentialInteractor = FakeCredentialInteractor(),
+        lockPatternUtils = lockPatternUtils,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 878e385..2a7e3e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -20,6 +20,7 @@
 
 import com.android.keyguard.logging.biometricUnlockLogger
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.dump.dumpManager
 import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.kosmos.Kosmos
@@ -27,17 +28,19 @@
 import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
+@OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.deviceEntryHapticsInteractor by
     Kosmos.Fixture {
         DeviceEntryHapticsInteractor(
-            deviceEntrySourceInteractor = deviceEntrySourceInteractor,
-            deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
-            deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
-            fingerprintPropertyRepository = fingerprintPropertyRepository,
             biometricSettingsRepository = biometricSettingsRepository,
+            deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
+            deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
+            deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+            fingerprintPropertyRepository = fingerprintPropertyRepository,
             keyEventInteractor = keyEventInteractor,
+            logger = biometricUnlockLogger,
             powerInteractor = powerInteractor,
             systemClock = systemClock,
-            logger = biometricUnlockLogger,
+            dumpManager = dumpManager,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt
index 0b9ec92..f91a044 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt
@@ -16,14 +16,34 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.biometrics.authController
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.statusbar.phone.dozeScrimController
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 @ExperimentalCoroutinesApi
 val Kosmos.deviceEntrySourceInteractor by
     Kosmos.Fixture {
         DeviceEntrySourceInteractor(
+            authenticationInteractor = authenticationInteractor,
+            authController = authController,
+            alternateBouncerInteractor = alternateBouncerInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+            deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
+            dozeScrimController = dozeScrimController,
+            keyguardBypassInteractor = keyguardBypassInteractor,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
             keyguardInteractor = keyguardInteractor,
+            sceneContainerOcclusionInteractor = sceneContainerOcclusionInteractor,
+            sceneInteractor = sceneInteractor,
+            dumpManager = dumpManager,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt
new file mode 100644
index 0000000..f469d74
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.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.dreams.homecontrols.service
+
+import android.content.ComponentName
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener
+
+/** Fake binder implementation of [IHomeControlsRemoteProxy] */
+class FakeIHomeControlsRemoteProxyBinder : IHomeControlsRemoteProxy.Stub() {
+    val callbacks = mutableSetOf<IOnControlsSettingsChangeListener>()
+
+    override fun registerListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) {
+        callbacks.add(callback)
+    }
+
+    override fun unregisterListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) {
+        callbacks.remove(callback)
+    }
+
+    fun notifyCallbacks(component: ComponentName, allowTrivialControlsOnLockscreen: Boolean) {
+        for (callback in callbacks.toSet()) {
+            callback.onControlsSettingsChanged(component, allowTrivialControlsOnLockscreen)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt
new file mode 100644
index 0000000..58ebbf4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.homeControlsRemoteProxy by
+    Kosmos.Fixture {
+        HomeControlsRemoteProxy(
+            bgScope = applicationCoroutineScope,
+            proxy = fakeHomeControlsRemoteBinder,
+            dumpManager = dumpManager,
+        )
+    }
+
+val Kosmos.fakeHomeControlsRemoteBinder by
+    Kosmos.Fixture<FakeIHomeControlsRemoteProxyBinder> { FakeIHomeControlsRemoteProxyBinder() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt
new file mode 100644
index 0000000..c85c888
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.dreams.homecontrols.service
+
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.logcatLogBuffer
+import org.mockito.kotlin.mock
+
+val Kosmos.remoteHomeControlsDataSourceDelegator by
+    Kosmos.Fixture {
+        RemoteHomeControlsDataSourceDelegator(
+            bgScope = applicationCoroutineScope,
+            serviceFactory = homeControlsRemoteServiceFactory,
+            logBuffer = logcatLogBuffer("HomeControlsDreamInteractor"),
+            dumpManager = dumpManager,
+        )
+    }
+
+var Kosmos.homeControlsRemoteServiceFactory by
+    Kosmos.Fixture<HomeControlsRemoteServiceComponent.Factory> { mock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt
new file mode 100644
index 0000000..f8ea022
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.dreams.homecontrols.shared.model
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class FakeHomeControlsDataSource : HomeControlsDataSource {
+
+    private val _componentInfo = MutableStateFlow<HomeControlsComponentInfo?>(null)
+
+    override val componentInfo: Flow<HomeControlsComponentInfo>
+        get() = _componentInfo.filterNotNull()
+
+    fun setComponentInfo(info: HomeControlsComponentInfo) {
+        _componentInfo.value = info
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt
new file mode 100644
index 0000000..942216b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.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.dreams.homecontrols.shared.model
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.homeControlsDataSource by
+    Kosmos.Fixture<HomeControlsDataSource> { fakeHomeControlsDataSource }
+
+val Kosmos.fakeHomeControlsDataSource by
+    Kosmos.Fixture<FakeHomeControlsDataSource> { FakeHomeControlsDataSource() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt
similarity index 75%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt
index 7d7841f..a1182a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt
@@ -13,21 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.dreams.homecontrols
+package com.android.systemui.dreams.homecontrols.system.domain.interactor
 
-import android.os.powerManager
-import android.service.dream.dreamManager
-import com.android.systemui.common.domain.interactor.packageChangeInteractor
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.panels.authorizedPanelsRepository
 import com.android.systemui.controls.panels.selectedComponentRepository
-import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.homeControlsComponentInteractor by
     Kosmos.Fixture {
@@ -37,10 +32,6 @@
             authorizedPanelsRepository = authorizedPanelsRepository,
             userRepository = fakeUserRepository,
             bgScope = applicationCoroutineScope,
-            systemClock = fakeSystemClock,
-            powerManager = powerManager,
-            dreamManager = dreamManager,
-            packageChangeInteractor = packageChangeInteractor,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.kt
new file mode 100644
index 0000000..fd882a8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.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.keyguard.data.quickaffordance
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.settings.userTracker
+
+val Kosmos.keyguardQuickAffordanceProviderClientFactory by
+    Kosmos.Fixture { FakeKeyguardQuickAffordanceProviderClientFactory(userTracker) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt
new file mode 100644
index 0000000..21d1a76
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.data.quickaffordance
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.settings.userFileManager
+import com.android.systemui.settings.userTracker
+
+val Kosmos.localUserSelectionManager by
+    Kosmos.Fixture {
+        KeyguardQuickAffordanceLocalUserSelectionManager(
+            context = applicationContext,
+            userFileManager = userFileManager,
+            userTracker = userTracker,
+            broadcastDispatcher = broadcastDispatcher,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.kt
new file mode 100644
index 0000000..ec63071
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.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.keyguard.data.quickaffordance
+
+import android.os.UserHandle
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.userTracker
+
+val Kosmos.remoteUserSelectionManager by
+    Kosmos.Fixture {
+        KeyguardQuickAffordanceRemoteUserSelectionManager(
+            scope = testScope.backgroundScope,
+            userTracker = userTracker,
+            clientFactory = keyguardQuickAffordanceProviderClientFactory,
+            userHandle = UserHandle.SYSTEM,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt
index 9bbb34c..84eea62 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.policy.devicePostureController
 
 val Kosmos.devicePostureRepository: DevicePostureRepository by
-    Kosmos.Fixture { FakeDevicePostureRepository() }
+    Kosmos.Fixture { DevicePostureRepositoryImpl(devicePostureController, testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 4976cc2..19e077c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -53,6 +53,15 @@
     private val initInLockscreen: Boolean = true,
 
     /**
+     * Initial value for [FakeKeyguardTransitionRepository.sendTransitionStepsOnStartTransition].
+     * This needs to be configurable in the constructor since some transitions are triggered on
+     * init, before a test has the chance to set sendTransitionStepsOnStartTransition to false.
+     */
+    private val initiallySendTransitionStepsOnStartTransition: Boolean = true,
+    private val testScope: TestScope,
+) : KeyguardTransitionRepository {
+
+    /**
      * If true, calls to [startTransition] will automatically emit STARTED, RUNNING, and FINISHED
      * transition steps from/to the given states.
      *
@@ -64,11 +73,9 @@
      *
      * If your test needs to make assertions at specific points between STARTED/FINISHED, or if it's
      * difficult to set up all of the conditions to make the transition interactors actually call
-     * startTransition, then construct a FakeKeyguardTransitionRepository with this value false.
+     * startTransition, set this value to false.
      */
-    private val sendTransitionStepsOnStartTransition: Boolean = true,
-    private val testScope: TestScope,
-) : KeyguardTransitionRepository {
+    var sendTransitionStepsOnStartTransition = initiallySendTransitionStepsOnStartTransition
 
     private val _transitions =
         MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@@ -77,7 +84,11 @@
     @Inject
     constructor(
         testScope: TestScope
-    ) : this(initInLockscreen = true, sendTransitionStepsOnStartTransition = true, testScope)
+    ) : this(
+        initInLockscreen = true,
+        initiallySendTransitionStepsOnStartTransition = true,
+        testScope
+    )
 
     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
         MutableStateFlow(
@@ -176,6 +187,20 @@
         testScheduler: TestCoroutineScheduler,
         throughTransitionState: TransitionState = TransitionState.FINISHED,
     ) {
+        val lastStep = _transitions.replayCache.lastOrNull()
+        if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) {
+            sendTransitionStep(
+                step =
+                TransitionStep(
+                    transitionState = TransitionState.CANCELED,
+                    from = lastStep.from,
+                    to = lastStep.to,
+                    value = 0f,
+                )
+            )
+            testScheduler.runCurrent()
+        }
+
         sendTransitionStep(
             step =
                 TransitionStep(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt
new file mode 100644
index 0000000..c91823c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.data.repository
+
+import android.content.testableContext
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.tuner.tunerService
+import com.android.systemui.util.mockito.withArgCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+val Kosmos.keyguardBypassRepository: KeyguardBypassRepository by Fixture {
+    KeyguardBypassRepository(
+        testableContext.resources,
+        biometricSettingsRepository,
+        devicePostureRepository,
+        dumpManager,
+        tunerService,
+        testDispatcher,
+    )
+}
+
+fun Kosmos.configureKeyguardBypass(
+    isBypassAvailable: Boolean? = null,
+    faceAuthEnrolledAndEnabled: Boolean = true,
+    faceUnlockBypassOverrideConfig: Int = 0, /* FACE_UNLOCK_BYPASS_NO_OVERRIDE */
+    faceAuthPostureConfig: Int = DEVICE_POSTURE_UNKNOWN,
+) {
+    when (isBypassAvailable) {
+        null -> {
+            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(faceAuthEnrolledAndEnabled)
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_unlock_bypass_override,
+                faceUnlockBypassOverrideConfig,
+            )
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_auth_supported_posture,
+                faceAuthPostureConfig,
+            )
+        }
+        true -> {
+            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_unlock_bypass_override,
+                1, /* FACE_UNLOCK_BYPASS_ALWAYS */
+            )
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_auth_supported_posture,
+                DEVICE_POSTURE_UNKNOWN,
+            )
+        }
+        false -> {
+            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_unlock_bypass_override,
+                2, /* FACE_UNLOCK_BYPASS_NEVER */
+            )
+            testableContext.orCreateTestableResources.addOverride(
+                R.integer.config_face_auth_supported_posture,
+                DEVICE_POSTURE_CLOSED,
+            )
+        }
+    }
+}
+
+fun DevicePostureController.verifyCallback() =
+    withArgCaptor<DevicePostureController.Callback> {
+        verify(this@verifyCallback).addCallback(capture())
+    }
+
+fun DevicePostureController.verifyNoCallback() =
+    verify(this@verifyNoCallback, never()).addCallback(any())
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt
new file mode 100644
index 0000000..a6d4ec5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.data.repository
+
+import android.content.applicationContext
+import android.os.UserHandle
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.data.quickaffordance.localUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.remoteUserSelectionManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.userTracker
+import org.mockito.kotlin.mock
+
+val Kosmos.keyguardQuickAffordanceRepository by Fixture {
+    KeyguardQuickAffordanceRepository(
+        appContext = applicationContext,
+        scope = testScope.backgroundScope,
+        localUserSelectionManager = localUserSelectionManager,
+        remoteUserSelectionManager = remoteUserSelectionManager,
+        userTracker = userTracker,
+        legacySettingSyncer = mock(),
+        configs = setOf(),
+        dumpManager = dumpManager,
+        userHandle = UserHandle.SYSTEM,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.kt
new file mode 100644
index 0000000..1851774
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.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.keyguard.data.repository
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.pulseExpansionRepository: PulseExpansionRepository by
+    Kosmos.Fixture { PulseExpansionRepository(dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt
new file mode 100644
index 0000000..59ef9df
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.data.repository.keyguardBypassRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+val Kosmos.keyguardBypassInteractor by Fixture {
+    KeyguardBypassInteractor(
+        keyguardBypassRepository,
+        alternateBouncerInteractor,
+        keyguardQuickAffordanceInteractor,
+        pulseExpansionInteractor,
+        sceneInteractor,
+        shadeInteractor,
+        dumpManager,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt
new file mode 100644
index 0000000..009d17e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.app.admin.devicePolicyManager
+import android.content.applicationContext
+import com.android.internal.widget.lockPatternUtils
+import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.dock.dockManager
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.keyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.settings.userTracker
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.policy.keyguardStateController
+import org.mockito.kotlin.mock
+
+var Kosmos.keyguardQuickAffordanceInteractor by Fixture {
+    KeyguardQuickAffordanceInteractor(
+        keyguardInteractor = keyguardInteractor,
+        shadeInteractor = shadeInteractor,
+        lockPatternUtils = lockPatternUtils,
+        keyguardStateController = keyguardStateController,
+        userTracker = userTracker,
+        activityStarter = activityStarter,
+        featureFlags = featureFlagsClassic,
+        repository = { keyguardQuickAffordanceRepository },
+        launchAnimator = dialogTransitionAnimator,
+        logger = mock<KeyguardQuickAffordancesLogger>(),
+        metricsLogger = mock<KeyguardQuickAffordancesMetricsLogger>(),
+        devicePolicyManager = devicePolicyManager,
+        dockManager = dockManager,
+        biometricSettingsRepository = biometricSettingsRepository,
+        backgroundDispatcher = testDispatcher,
+        appContext = applicationContext,
+        sceneInteractor = { sceneInteractor },
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.kt
new file mode 100644
index 0000000..f02e0a4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.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.keyguard.domain.interactor
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.data.repository.pulseExpansionRepository
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.pulseExpansionInteractor: PulseExpansionInteractor by
+    Kosmos.Fixture { PulseExpansionInteractor(pulseExpansionRepository, dumpManager) }
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 3c87106..3ab686d 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
@@ -21,6 +21,7 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -41,6 +42,7 @@
         communalInteractor = communalInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         notificationsKeyguardInteractor = notificationsKeyguardInteractor,
+        pulseExpansionInteractor = pulseExpansionInteractor,
         aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel,
         notificationShadeWindowModel = notificationShadeWindowModel,
         alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 522c387..5eaa198 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
 import com.android.systemui.model.sceneContainerPlugin
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.power.data.repository.fakePowerRepository
@@ -124,6 +125,7 @@
     val sceneBackInteractor by lazy { kosmos.sceneBackInteractor }
     val falsingCollector by lazy { kosmos.falsingCollector }
     val powerInteractor by lazy { kosmos.powerInteractor }
+    val pulseExpansionInteractor by lazy { kosmos.pulseExpansionInteractor }
     val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
     val deviceEntryUdfpsInteractor by lazy { kosmos.deviceEntryUdfpsInteractor }
     val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.kt
new file mode 100644
index 0000000..c337298
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.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.media.controls.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+/** Empty mock */
+val Kosmos.legacyMediaDataManagerImpl by Kosmos.Fixture { mock<LegacyMediaDataManagerImpl>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.kt
new file mode 100644
index 0000000..f733da1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.mediaDataManager: MediaDataManager by Kosmos.Fixture()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.kt
new file mode 100644
index 0000000..3c35ce9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.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.media.controls.ui.controller
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+/** Empty mock */
+val Kosmos.mediaCarouselController by Kosmos.Fixture { mock<MediaCarouselController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.kt
new file mode 100644
index 0000000..9f5a832
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.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.media.controls.ui.controller
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.core.FakeLogBuffer
+
+val Kosmos.mediaCarouselControllerLogger by
+    Kosmos.Fixture { MediaCarouselControllerLogger(FakeLogBuffer.Factory.create()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt
index 7c24b4c..624e174 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt
@@ -16,8 +16,15 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.content.testableContext
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.animation.UniqueObjectHostView
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
 
-var Kosmos.mediaHierarchyManager by Fixture { mock<MediaHierarchyManager>() }
+var Kosmos.mediaHierarchyManager by Fixture {
+    mock<MediaHierarchyManager> {
+        on { register(any()) }.thenAnswer { UniqueObjectHostView(this@Fixture.testableContext) }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.kt
new file mode 100644
index 0000000..8b02c57
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.media.controls.ui.controller
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaHostStatesManager by Kosmos.Fixture { MediaHostStatesManager() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt
new file mode 100644
index 0000000..9638e11
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.media.controls.ui.view
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.domain.pipeline.mediaDataManager
+import com.android.systemui.media.controls.ui.controller.mediaCarouselController
+import com.android.systemui.media.controls.ui.controller.mediaCarouselControllerLogger
+import com.android.systemui.media.controls.ui.controller.mediaHierarchyManager
+import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager
+import java.util.function.Supplier
+
+private val Kosmos.mediaHostProvider by
+    Kosmos.Fixture {
+        Supplier<MediaHost> {
+            MediaHost(
+                MediaHost.MediaHostStateHolder(),
+                mediaHierarchyManager,
+                mediaDataManager,
+                mediaHostStatesManager,
+                mediaCarouselController,
+                mediaCarouselControllerLogger,
+            )
+        }
+    }
+
+val Kosmos.qqsMediaHost by Kosmos.Fixture { mediaHostProvider.get() }
+val Kosmos.qsMediaHost by Kosmos.Fixture { mediaHostProvider.get() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.kt
new file mode 100644
index 0000000..fb8ffb0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.qs.composefragment.dagger
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.usingMediaInComposeFragment by Kosmos.Fixture<Boolean>()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index 462b408..bda3192 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -22,10 +22,13 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.ui.view.qqsMediaHost
+import com.android.systemui.media.controls.ui.view.qsMediaHost
+import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
-import com.android.systemui.qs.panels.ui.viewmodel.paginatedGridViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.inFirstPageViewModel
 import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.transition.largeScreenShadeInterpolator
@@ -53,7 +56,10 @@
                     configurationInteractor,
                     largeScreenHeaderHelper,
                     tileSquishinessInteractor,
-                    paginatedGridViewModel,
+                    inFirstPageViewModel,
+                    qqsMediaHost,
+                    qsMediaHost,
+                    usingMediaInComposeFragment,
                     lifecycleScope,
                 )
             }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.kt
new file mode 100644
index 0000000..1fa74ca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.inFirstPageViewModel by Kosmos.Fixture { InFirstPageViewModel() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index 48ef57e..5c8ca83 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor
 
 val Kosmos.paginatedGridViewModel by
@@ -26,6 +25,6 @@
             iconTilesViewModel,
             qsColumnsViewModel,
             paginatedGridInteractor,
-            applicationCoroutineScope,
+            inFirstPageViewModel,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
index 41ee260..20be5c6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt
@@ -18,19 +18,21 @@
 
 import com.android.systemui.haptics.msdl.tileHapticsViewModelFactoryProvider
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.qs.panels.domain.interactor.quickQuickSettingsRowInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 
-val Kosmos.quickQuickSettingsViewModel by
+val Kosmos.quickQuickSettingsViewModelFactory by
     Kosmos.Fixture {
-        QuickQuickSettingsViewModel(
-            currentTilesInteractor,
-            qsColumnsViewModel,
-            quickQuickSettingsRowInteractor,
-            tileSquishinessViewModel,
-            iconTilesViewModel,
-            applicationCoroutineScope,
-            tileHapticsViewModelFactoryProvider,
-        )
+        object : QuickQuickSettingsViewModel.Factory {
+            override fun create(): QuickQuickSettingsViewModel {
+                return QuickQuickSettingsViewModel(
+                    currentTilesInteractor,
+                    qsColumnsViewModel,
+                    quickQuickSettingsRowInteractor,
+                    tileSquishinessViewModel,
+                    iconTilesViewModel,
+                    tileHapticsViewModelFactoryProvider,
+                )
+            }
+        }
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
index 6ded751..ce103ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFactory
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory
 import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel
 
 val Kosmos.quickSettingsContainerViewModelFactory by
@@ -30,10 +30,10 @@
             ): QuickSettingsContainerViewModel {
                 return QuickSettingsContainerViewModel(
                     brightnessSliderViewModelFactory,
+                    quickQuickSettingsViewModelFactory,
                     supportsBrightnessMirroring,
                     tileGridViewModel,
                     editModeViewModel,
-                    quickQuickSettingsViewModel,
                 )
             }
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.kt
new file mode 100644
index 0000000..d6dd867
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.shade
+
+import com.android.systemui.camera.cameraGestureHelper
+import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.keyguardBypassController
+
+val Kosmos.cameraLauncher by
+    Kosmos.Fixture {
+        CameraLauncher(cameraGestureHelper, keyguardBypassController) {
+            keyguardQuickAffordanceInteractor
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
index 61a38b8..9c2a2be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
@@ -22,7 +22,5 @@
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 
 val Kosmos.notificationsKeyguardInteractor by Fixture {
-    NotificationsKeyguardInteractor(
-        repository = notificationsKeyguardViewStateRepository,
-    )
+    NotificationsKeyguardInteractor(repository = notificationsKeyguardViewStateRepository)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt
index 8785256..87ea147 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt
@@ -19,6 +19,7 @@
 import android.content.applicationContext
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -43,5 +44,6 @@
             mock<UnlockedScreenOffAnimationController>(),
             primaryBouncerInteractor,
             alternateBouncerInteractor,
+            communalSceneInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.kt
new file mode 100644
index 0000000..9bc3ae9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.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.tuner
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import org.mockito.kotlin.mock
+
+val Kosmos.tunerService by Fixture { mock<TunerService>() }
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 7fa0ef1..a1243e3 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -35,18 +35,18 @@
     },
     {
       "name": "CarLibHostUnitTest",
-      "host": true,
-      "keywords": ["automotive_code_coverage"]
+      "keywords": ["automotive_code_coverage"],
+      "host": true
     },
     {
       "name": "CarServiceHostUnitTest",
-      "host": true,
-      "keywords": ["automotive_code_coverage"]
+      "keywords": ["automotive_code_coverage"],
+      "host": true
     },
     {
       "name": "CarSystemUIRavenTests",
-      "host": true,
-      "keywords": ["automotive_code_coverage"]
+      "keywords": ["automotive_code_coverage"],
+      "host": true
     },
     {
       "name": "CtsAccountManagerTestCasesRavenwood",
@@ -65,6 +65,10 @@
       "host": true
     },
     {
+      "name": "CtsDeviceConfigTestCasesRavenwood",
+      "host": true
+    },
+    {
       "name": "CtsGraphicsTestCasesRavenwood",
       "host": true
     },
diff --git a/ravenwood/scripts/update-test-mapping.sh b/ravenwood/scripts/update-test-mapping.sh
index e478b50..ab37baf 100755
--- a/ravenwood/scripts/update-test-mapping.sh
+++ b/ravenwood/scripts/update-test-mapping.sh
@@ -23,6 +23,14 @@
 # Tests that shouldn't be in presubmit.
 EXEMPT='^(SystemUiRavenTests)$'
 
+is_car() {
+    local module="$1"
+
+    # If the module name starts with "Car", then it's a test for "Car".
+    [[ "$module" =~ ^Car ]]
+    return $?
+}
+
 main() {
     local script_name="${0##*/}"
     local script_dir="${0%/*}"
@@ -62,6 +70,10 @@
             fi
             echo "    {"
             echo "      \"name\": \"${tests[$i]}\","
+            if is_car "${tests[$i]}"; then
+                echo '      "keywords": ["automotive_code_coverage"],'
+            fi
+
             echo "      \"host\": true"
             echo "    }$comma"
 
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index a26fe66..70c1d78 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -2,6 +2,9 @@
 
 com.android.internal.ravenwood.*
 
+com.android.server.FgThread
+com.android.server.ServiceThread
+
 com.android.internal.display.BrightnessSynchronizer
 com.android.internal.util.ArrayUtils
 com.android.internal.logging.MetricsLogger
diff --git a/services/Android.bp b/services/Android.bp
index f04c692..899e224 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -188,6 +188,28 @@
     },
 }
 
+// Conditionally add crashrecovery stubs library
+soong_config_module_type {
+    name: "crashrecovery_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: [
+        "release_crashrecovery_module",
+    ],
+    properties: [
+        "libs",
+    ],
+}
+
+crashrecovery_java_defaults {
+    name: "services_crashrecovery_stubs_conditionally",
+    soong_config_variables: {
+        release_crashrecovery_module: {
+            libs: ["service-crashrecovery.stubs.system_server"],
+        },
+    },
+}
+
 // merge all required services into one jar
 // ============================================================
 soong_config_module_type {
@@ -213,6 +235,7 @@
     defaults: [
         "services_java_defaults",
         "art_profile_java_defaults",
+        "services_crashrecovery_stubs_conditionally",
     ],
     installable: true,
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index c6fe497..974cba2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -52,6 +52,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
@@ -3897,6 +3898,7 @@
                 userState.getShortcutTargetsLocked(HARDWARE);
         final Set<String> qsShortcutTargets =
                 userState.getShortcutTargetsLocked(QUICK_SETTINGS);
+        final Set<String> shortcutTargets = userState.getShortcutTargetsLocked(ALL);
         userState.mEnabledServices.forEach(componentName -> {
             if (packageName != null && componentName != null
                     && !packageName.equals(componentName.getPackageName())) {
@@ -3917,7 +3919,11 @@
             if (TextUtils.isEmpty(serviceName)) {
                 return;
             }
-            if (doesShortcutTargetsStringContain(buttonTargets, serviceName)
+            if (android.provider.Flags.a11yStandaloneGestureEnabled()) {
+                if (doesShortcutTargetsStringContain(shortcutTargets, serviceName)) {
+                    return;
+                }
+            } else if (doesShortcutTargetsStringContain(buttonTargets, serviceName)
                     || doesShortcutTargetsStringContain(shortcutKeyTargets, serviceName)
                     || doesShortcutTargetsStringContain(qsShortcutTargets, serviceName)) {
                 return;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 0bf7ec00..67b4063 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -106,21 +106,17 @@
 
     final Set<ComponentName> mTouchExplorationGrantedServices = new HashSet<>();
 
-    private final ArraySet<String> mAccessibilityShortcutKeyTargets = new ArraySet<>();
-
-    private final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>();
-    private final ArraySet<String> mAccessibilityGestureTargets = new ArraySet<>();
-    private final ArraySet<String> mAccessibilityQsTargets = new ArraySet<>();
+    private final HashMap<Integer, ArraySet<String>> mShortcutTargets = new HashMap<>();
 
     /**
-     * The QuickSettings tiles in the QS Panel. This can be different from
-     * {@link #mAccessibilityQsTargets} in that {@link #mA11yTilesInQsPanel} stores the
+     * The QuickSettings tiles in the QS Panel. This can be different from the QS targets in
+     * {@link #mShortcutTargets} in that {@link #mA11yTilesInQsPanel} stores the
      * TileService's or the a11y framework tile component names (e.g.
      * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the
      * A11y Feature's component names.
      * <p/>
      * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas
-     * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and
+     * {@link #mShortcutTargets} stores the targets that configured qs as their shortcut and
      * also grant full device control permission.
      */
     private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>();
@@ -208,6 +204,11 @@
         mSupportWindowMagnification = mContext.getResources().getBoolean(
                 R.bool.config_magnification_area) && mContext.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_WINDOW_MAGNIFICATION);
+
+        mShortcutTargets.put(HARDWARE, new ArraySet<>());
+        mShortcutTargets.put(SOFTWARE, new ArraySet<>());
+        mShortcutTargets.put(GESTURE, new ArraySet<>());
+        mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>());
     }
 
     boolean isHandlingAccessibilityEventsLocked() {
@@ -233,10 +234,7 @@
         // Clear state persisted in settings.
         mEnabledServices.clear();
         mTouchExplorationGrantedServices.clear();
-        mAccessibilityShortcutKeyTargets.clear();
-        mAccessibilityButtonTargets.clear();
-        mAccessibilityGestureTargets.clear();
-        mAccessibilityQsTargets.clear();
+        mShortcutTargets.forEach((type, targets) -> targets.clear());
         mA11yTilesInQsPanel.clear();
         mTargetAssignedToAccessibilityButton = null;
         mIsTouchExplorationEnabled = false;
@@ -541,7 +539,7 @@
     private void dumpShortcutTargets(
             PrintWriter pw, @UserShortcutType int shortcutType, String name) {
         pw.append("     ").append(name).append(":{");
-        ArraySet<String> targets = getShortcutTargetsInternalLocked(shortcutType);
+        ArraySet<String> targets = getShortcutTargetsLocked(shortcutType);
         int size = targets.size();
         for (int i = 0; i < size; i++) {
             if (i > 0) {
@@ -712,7 +710,7 @@
      */
     public boolean isShortcutMagnificationEnabledLocked() {
         for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) {
-            if (getShortcutTargetsInternalLocked(shortcutType)
+            if (getShortcutTargetsLocked(shortcutType)
                     .contains(MAGNIFICATION_CONTROLLER_NAME)) {
                 return true;
             }
@@ -788,43 +786,29 @@
     }
 
     /**
-     * Disable both shortcuts' magnification function.
-     */
-    public void disableShortcutMagnificationLocked() {
-        mAccessibilityShortcutKeyTargets.remove(MAGNIFICATION_CONTROLLER_NAME);
-        mAccessibilityButtonTargets.remove(MAGNIFICATION_CONTROLLER_NAME);
-    }
-
-    /**
      * Returns a set which contains the flattened component names and the system class names
-     * assigned to the given shortcut. The set is a defensive copy. To apply any changes to the set,
-     * use {@link #updateShortcutTargetsLocked(Set, int)}
+     * assigned to the given shortcut. <strong>The set is a defensive copy.</strong>
+     * To apply any changes to the set, use {@link #updateShortcutTargetsLocked(Set, int)}
      *
-     * @param shortcutType The shortcut type.
+     * @param shortcutTypes The shortcut type or types (in bitmask format).
      * @return The array set of the strings
      */
-    public ArraySet<String> getShortcutTargetsLocked(@UserShortcutType int shortcutType) {
-        return new ArraySet<>(getShortcutTargetsInternalLocked(shortcutType));
-    }
-
-    private ArraySet<String> getShortcutTargetsInternalLocked(@UserShortcutType int shortcutType) {
-        if (shortcutType == HARDWARE) {
-            return mAccessibilityShortcutKeyTargets;
-        } else if (shortcutType == SOFTWARE) {
-            return mAccessibilityButtonTargets;
-        } else if (shortcutType == GESTURE) {
-            return mAccessibilityGestureTargets;
-        } else if (shortcutType == QUICK_SETTINGS) {
-            return mAccessibilityQsTargets;
-        } else if ((shortcutType == TRIPLETAP
-                && isMagnificationSingleFingerTripleTapEnabledLocked()) || (
-                shortcutType == TWOFINGER_DOUBLETAP
-                        && isMagnificationTwoFingerTripleTapEnabledLocked())) {
-            ArraySet<String> targets = new ArraySet<>();
-            targets.add(MAGNIFICATION_CONTROLLER_NAME);
-            return targets;
+    public ArraySet<String> getShortcutTargetsLocked(int shortcutTypes) {
+        ArraySet<String> targets = new ArraySet<>();
+        for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) {
+            if ((shortcutTypes & shortcutType) != shortcutType) {
+                continue;
+            }
+            if ((shortcutType == TRIPLETAP
+                    && isMagnificationSingleFingerTripleTapEnabledLocked()) || (
+                    shortcutType == TWOFINGER_DOUBLETAP
+                            && isMagnificationTwoFingerTripleTapEnabledLocked())) {
+                targets.add(MAGNIFICATION_CONTROLLER_NAME);
+            } else if (mShortcutTargets.containsKey(shortcutType)) {
+                targets.addAll(mShortcutTargets.get(shortcutType));
+            }
         }
-        return new ArraySet<>();
+        return targets;
     }
 
     /**
@@ -843,8 +827,10 @@
         if ((shortcutType & mask) != 0) {
             throw new IllegalArgumentException("Tap shortcuts cannot be updated with target sets.");
         }
-
-        final Set<String> currentTargets = getShortcutTargetsInternalLocked(shortcutType);
+        if (!mShortcutTargets.containsKey(shortcutType)) {
+            mShortcutTargets.put(shortcutType, new ArraySet<>());
+        }
+        ArraySet<String> currentTargets = mShortcutTargets.get(shortcutType);
         if (newTargets.equals(currentTargets)) {
             return false;
         }
@@ -904,7 +890,7 @@
         }
 
         // getting internal set lets us directly modify targets, as it's not a copy.
-        Set<String> targets = getShortcutTargetsInternalLocked(shortcutType);
+        Set<String> targets = mShortcutTargets.get(shortcutType);
         return targets.removeIf(name -> {
             ComponentName componentName;
             if (name == null
@@ -1169,13 +1155,6 @@
         );
     }
 
-    /**
-     * Returns a copy of the targets which has qs shortcut turned on
-     */
-    public ArraySet<String> getA11yQsTargets() {
-        return new ArraySet<>(mAccessibilityQsTargets);
-    }
-
     public void updateA11yTilesInQsPanelLocked(Set<ComponentName> componentNames) {
         mA11yTilesInQsPanel.clear();
         mA11yTilesInQsPanel.addAll(componentNames);
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index c9f8929..b52c6505 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -92,6 +92,7 @@
 import com.android.server.contentcapture.ContentCaptureManagerInternal;
 import com.android.server.infra.AbstractPerUserSystemService;
 import com.android.server.inputmethod.InputMethodManagerInternal;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.io.PrintWriter;
@@ -192,6 +193,8 @@
 
     private final ContentCaptureManagerInternal mContentCaptureManagerInternal;
 
+    private final UserManagerInternal mUserManagerInternal;
+
     private final DisabledInfoCache mDisabledInfoCache;
 
     AutofillManagerServiceImpl(AutofillManagerService master, Object lock,
@@ -208,6 +211,7 @@
         mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class);
         mContentCaptureManagerInternal = LocalServices.getService(
                 ContentCaptureManagerInternal.class);
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         mDisabledInfoCache = disableCache;
         updateLocked(disabled);
     }
@@ -379,6 +383,13 @@
             return 0;
         }
 
+        // TODO(b/376482880): remove this check once autofill service supports visible
+        // background users.
+        if (mUserManagerInternal.isVisibleBackgroundFullUser(mUserId)) {
+            Slog.d(TAG, "Currently, autofill service does not support visible background users.");
+            return 0;
+        }
+
         if (!forAugmentedAutofillOnly && isAutofillDisabledLocked(clientActivity)) {
             // Standard autofill is enabled, but service disabled autofill for this activity; that
             // means no session, unless the activity is allowlisted for augmented autofill
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index a960015..51034d2 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -31,13 +31,16 @@
 
 import static com.android.internal.util.CollectionUtils.any;
 import static com.android.internal.util.Preconditions.checkState;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
+import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
 import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
 
 import static java.util.Objects.requireNonNull;
+import static java.util.concurrent.TimeUnit.MINUTES;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -66,22 +69,31 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.net.MacAddress;
+import android.net.NetworkPolicyManager;
 import android.os.Binder;
+import android.os.Environment;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.PowerExemptionManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.permission.flags.Flags;
+import android.util.ArraySet;
 import android.util.ExceptionUtils;
 import android.util.Slog;
 
+import com.android.internal.app.IAppOpsService;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.server.FgThread;
@@ -102,27 +114,35 @@
 import com.android.server.companion.devicepresence.ObservableUuid;
 import com.android.server.companion.devicepresence.ObservableUuidStore;
 import com.android.server.companion.transport.CompanionTransportManager;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.Set;
 
 @SuppressLint("LongLogTag")
 public class CompanionDeviceManagerService extends SystemService {
     private static final String TAG = "CDM_CompanionDeviceManagerService";
 
     private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
+
+    private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
+    private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
     private static final int MAX_CN_LENGTH = 500;
 
+    private final ActivityTaskManagerInternal mAtmInternal;
+    private final ActivityManagerInternal mAmInternal;
+    private final IAppOpsService mAppOpsManager;
+    private final PowerExemptionManager mPowerExemptionManager;
+    private final PackageManagerInternal mPackageManagerInternal;
+
     private final AssociationStore mAssociationStore;
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     private final ObservableUuidStore mObservableUuidStore;
-
-    private final CompanionExemptionProcessor mCompanionExemptionProcessor;
     private final AssociationRequestsProcessor mAssociationRequestsProcessor;
     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
     private final BackupRestoreProcessor mBackupRestoreProcessor;
@@ -136,15 +156,12 @@
         super(context);
 
         final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
-        final PowerExemptionManager powerExemptionManager = context.getSystemService(
-                PowerExemptionManager.class);
-        final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
-        final ActivityTaskManagerInternal atmInternal = LocalServices.getService(
-                ActivityTaskManagerInternal.class);
-        final ActivityManagerInternal amInternal = LocalServices.getService(
-                ActivityManagerInternal.class);
-        final PackageManagerInternal packageManagerInternal = LocalServices.getService(
-                PackageManagerInternal.class);
+        mPowerExemptionManager = context.getSystemService(PowerExemptionManager.class);
+        mAppOpsManager = IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
+        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         final UserManager userManager = context.getSystemService(UserManager.class);
         final PowerManagerInternal powerManagerInternal = LocalServices.getService(
                 PowerManagerInternal.class);
@@ -156,29 +173,25 @@
 
         // Init processors
         mAssociationRequestsProcessor = new AssociationRequestsProcessor(context,
-                packageManagerInternal, mAssociationStore);
-        mBackupRestoreProcessor = new BackupRestoreProcessor(context, packageManagerInternal,
+                mPackageManagerInternal, mAssociationStore);
+        mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal,
                 mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
                 mAssociationRequestsProcessor);
 
         mCompanionAppBinder = new CompanionAppBinder(context);
 
-        mCompanionExemptionProcessor = new CompanionExemptionProcessor(context,
-                powerExemptionManager, appOpsManager, packageManagerInternal, atmInternal,
-                amInternal, mAssociationStore);
-
         mDevicePresenceProcessor = new DevicePresenceProcessor(context,
                 mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
-                powerManagerInternal, mCompanionExemptionProcessor);
+                powerManagerInternal);
 
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
 
         mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
-                mAssociationStore, packageManagerInternal, mDevicePresenceProcessor,
+                mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
                 mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);
 
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
-                packageManagerInternal, mAssociationStore,
+                mPackageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
 
         // TODO(b/279663946): move context sync to a dedicated system service
@@ -189,6 +202,7 @@
     public void onStart() {
         // Init association stores
         mAssociationStore.refreshCache();
+        mAssociationStore.registerLocalListener(mAssociationStoreChangeListener);
 
         // Init UUID store
         mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
@@ -226,11 +240,11 @@
 
         if (associations.isEmpty()) return;
 
-        mCompanionExemptionProcessor.updateAtm(userId, associations);
+        updateAtm(userId, associations);
 
-        try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
-            executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions);
-        }
+        BackgroundThread.getHandler().sendMessageDelayed(
+                obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
+                MINUTES.toMillis(10));
     }
 
     @Override
@@ -248,12 +262,9 @@
         if (!associationsForPackage.isEmpty()) {
             Slog.i(TAG, "Package removed or data cleared for user=[" + userId + "], package=["
                     + packageName + "]. Cleaning up CDM data...");
-
-            for (AssociationInfo association : associationsForPackage) {
-                mDisassociationProcessor.disassociate(association.getId());
-            }
-
-            mCompanionAppBinder.onPackagesChanged(userId, packageName);
+        }
+        for (AssociationInfo association : associationsForPackage) {
+            mDisassociationProcessor.disassociate(association.getId());
         }
 
         // Clear observable UUIDs for the package.
@@ -262,16 +273,19 @@
         for (ObservableUuid uuid : uuidsTobeObserved) {
             mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
         }
+
+        mCompanionAppBinder.onPackagesChanged(userId);
     }
 
     private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
-        final List<AssociationInfo> associations =
+        final List<AssociationInfo> associationsForPackage =
                 mAssociationStore.getAssociationsByPackage(userId, packageName);
-        if (!associations.isEmpty()) {
-            mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
-
-            mCompanionAppBinder.onPackagesChanged(userId, packageName);
+        for (AssociationInfo association : associationsForPackage) {
+            updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
+                    association.getPackageName());
         }
+
+        mCompanionAppBinder.onPackagesChanged(userId);
     }
 
     private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -751,6 +765,130 @@
         }
     }
 
+    /**
+     * Update special access for the association's package
+     */
+    public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) {
+        final PackageInfo packageInfo =
+                getPackageInfo(getContext(), userId, packageName);
+
+        Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
+    }
+
+    private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+        if (packageInfo == null) {
+            return;
+        }
+
+        if (containsEither(packageInfo.requestedPermissions,
+                android.Manifest.permission.RUN_IN_BACKGROUND,
+                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
+            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
+        } else {
+            try {
+                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
+            } catch (UnsupportedOperationException e) {
+                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
+                        + " whitelist. It might due to the package is whitelisted by the system.");
+            }
+        }
+
+        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
+        try {
+            if (containsEither(packageInfo.requestedPermissions,
+                    android.Manifest.permission.USE_DATA_IN_BACKGROUND,
+                    android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
+                networkPolicyManager.addUidPolicy(
+                        packageInfo.applicationInfo.uid,
+                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+            } else {
+                networkPolicyManager.removeUidPolicy(
+                        packageInfo.applicationInfo.uid,
+                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+            }
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, e.getMessage());
+        }
+
+        exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);
+    }
+
+    private void exemptFromAutoRevoke(String packageName, int uid) {
+        try {
+            mAppOpsManager.setMode(
+                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
+                    uid,
+                    packageName,
+                    AppOpsManager.MODE_IGNORED);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e);
+        }
+    }
+
+    private void updateAtm(int userId, List<AssociationInfo> associations) {
+        final Set<Integer> companionAppUids = new ArraySet<>();
+        for (AssociationInfo association : associations) {
+            final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
+                    0, userId);
+            if (uid >= 0) {
+                companionAppUids.add(uid);
+            }
+        }
+        if (mAtmInternal != null) {
+            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+        }
+        if (mAmInternal != null) {
+            // Make a copy of the set and send it to ActivityManager.
+            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
+        }
+    }
+
+    private void maybeGrantAutoRevokeExemptions() {
+        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
+
+        PackageManager pm = getContext().getPackageManager();
+        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
+            SharedPreferences pref = getContext().getSharedPreferences(
+                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
+                    Context.MODE_PRIVATE);
+            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
+                continue;
+            }
+
+            try {
+                final List<AssociationInfo> associations =
+                        mAssociationStore.getActiveAssociationsByUser(userId);
+                for (AssociationInfo a : associations) {
+                    try {
+                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
+                        exemptFromAutoRevoke(a.getPackageName(), uid);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
+                    }
+                }
+            } finally {
+                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
+            }
+        }
+    }
+
+    private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
+            new AssociationStore.OnChangeListener() {
+                @Override
+                public void onAssociationChanged(int changeType, AssociationInfo association) {
+                    Slog.d(TAG, "onAssociationChanged changeType=[" + changeType
+                            + "], association=[" + association);
+
+                    final int userId = association.getUserId();
+                    final List<AssociationInfo> updatedAssociations =
+                            mAssociationStore.getActiveAssociationsByUser(userId);
+
+                    updateAtm(userId, updatedAssociations);
+                    updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
+                            association.getPackageName());
+                }
+            };
+
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
         public void onPackageRemoved(String packageName, int uid) {
@@ -773,6 +911,10 @@
         }
     };
 
+    private static <T> boolean containsEither(T[] array, T a, T b) {
+        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
+    }
+
     private class LocalService implements CompanionDeviceManagerServiceInternal {
 
         @Override
diff --git a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
deleted file mode 100644
index 4969ffbf..0000000
--- a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * 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.server.companion;
-
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.MODE_IGNORED;
-
-import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManagerInternal;
-import android.app.AppOpsManager;
-import android.companion.AssociationInfo;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
-import android.net.NetworkPolicyManager;
-import android.os.Binder;
-import android.os.Environment;
-import android.os.PowerExemptionManager;
-import android.util.ArraySet;
-import android.util.Slog;
-
-import com.android.internal.util.ArrayUtils;
-import com.android.server.LocalServices;
-import com.android.server.companion.association.AssociationStore;
-import com.android.server.pm.UserManagerInternal;
-import com.android.server.wm.ActivityTaskManagerInternal;
-
-import java.io.File;
-import java.util.List;
-import java.util.Set;
-
-@SuppressLint("LongLogTag")
-public class CompanionExemptionProcessor {
-
-    private static final String TAG = "CDM_CompanionExemptionProcessor";
-
-    private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
-    private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
-
-    private final Context mContext;
-    private final PowerExemptionManager mPowerExemptionManager;
-    private final AppOpsManager mAppOpsManager;
-    private final PackageManagerInternal mPackageManager;
-    private final ActivityTaskManagerInternal mAtmInternal;
-    private final ActivityManagerInternal mAmInternal;
-    private final AssociationStore mAssociationStore;
-
-    public CompanionExemptionProcessor(Context context, PowerExemptionManager powerExemptionManager,
-            AppOpsManager appOpsManager, PackageManagerInternal packageManager,
-            ActivityTaskManagerInternal atmInternal, ActivityManagerInternal amInternal,
-            AssociationStore associationStore) {
-        mContext = context;
-        mPowerExemptionManager = powerExemptionManager;
-        mAppOpsManager = appOpsManager;
-        mPackageManager = packageManager;
-        mAtmInternal = atmInternal;
-        mAmInternal = amInternal;
-        mAssociationStore = associationStore;
-
-        mAssociationStore.registerLocalListener(new AssociationStore.OnChangeListener() {
-            @Override
-            public void onAssociationChanged(int changeType, AssociationInfo association) {
-                final int userId = association.getUserId();
-                final List<AssociationInfo> updatedAssociations =
-                        mAssociationStore.getActiveAssociationsByUser(userId);
-
-                updateAtm(userId, updatedAssociations);
-            }
-        });
-    }
-
-    /**
-     * Update ActivityManager and ActivityTaskManager exemptions
-     */
-    public void updateAtm(int userId, List<AssociationInfo> associations) {
-        final Set<Integer> companionAppUids = new ArraySet<>();
-        for (AssociationInfo association : associations) {
-            int uid = mPackageManager.getPackageUid(association.getPackageName(), 0, userId);
-            if (uid >= 0) {
-                companionAppUids.add(uid);
-            }
-        }
-        if (mAtmInternal != null) {
-            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
-        }
-        if (mAmInternal != null) {
-            // Make a copy of the set and send it to ActivityManager.
-            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
-        }
-    }
-
-    /**
-     * Update special access for the association's package
-     */
-    public void exemptPackage(int userId, String packageName, boolean hasPresentDevices) {
-        Slog.i(TAG, "Exempting package [" + packageName + "]...");
-
-        final PackageInfo packageInfo = getPackageInfo(mContext, userId, packageName);
-
-        Binder.withCleanCallingIdentity(
-                () -> exemptPackageAsSystem(userId, packageInfo, hasPresentDevices));
-    }
-
-    @SuppressLint("MissingPermission")
-    private void exemptPackageAsSystem(int userId, PackageInfo packageInfo,
-            boolean hasPresentDevices) {
-        if (packageInfo == null) {
-            return;
-        }
-
-        // If the app has run-in-bg permission and present devices, add it to power saver allowlist.
-        if (containsEither(packageInfo.requestedPermissions,
-                android.Manifest.permission.RUN_IN_BACKGROUND,
-                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
-                && hasPresentDevices) {
-            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
-        } else {
-            try {
-                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
-            } catch (UnsupportedOperationException e) {
-                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
-                        + " allowlist. It might be due to the package being allowlisted by the"
-                        + " system.");
-            }
-        }
-
-        // If the app has run-in-bg permission and present device, allow metered network use.
-        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(mContext);
-        try {
-            if (containsEither(packageInfo.requestedPermissions,
-                    android.Manifest.permission.USE_DATA_IN_BACKGROUND,
-                    android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
-                    && hasPresentDevices) {
-                networkPolicyManager.addUidPolicy(
-                        packageInfo.applicationInfo.uid,
-                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
-            } else {
-                networkPolicyManager.removeUidPolicy(
-                        packageInfo.applicationInfo.uid,
-                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
-            }
-        } catch (IllegalArgumentException e) {
-            Slog.e(TAG, e.getMessage());
-        }
-
-        updateAutoRevokeExemption(packageInfo.packageName, packageInfo.applicationInfo.uid,
-                !mAssociationStore.getActiveAssociationsByPackage(userId,
-                        packageInfo.packageName).isEmpty());
-    }
-
-    /**
-     * Update auto revoke exemptions.
-     * If the app has any association, exempt it from permission auto revoke.
-     */
-    public void updateAutoRevokeExemptions() {
-        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
-
-        PackageManager pm = mContext.getPackageManager();
-        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
-            SharedPreferences pref = mContext.getSharedPreferences(
-                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
-                    Context.MODE_PRIVATE);
-            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
-                continue;
-            }
-
-            try {
-                final List<AssociationInfo> associations =
-                        mAssociationStore.getActiveAssociationsByUser(userId);
-                for (AssociationInfo a : associations) {
-                    try {
-                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
-                        updateAutoRevokeExemption(a.getPackageName(), uid, true);
-                    } catch (PackageManager.NameNotFoundException e) {
-                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
-                    }
-                }
-            } finally {
-                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
-            }
-        }
-    }
-
-    @SuppressLint("MissingPermission")
-    private void updateAutoRevokeExemption(String packageName, int uid, boolean hasAssociations) {
-        try {
-            mAppOpsManager.setMode(
-                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
-                    uid,
-                    packageName,
-                    hasAssociations ? MODE_IGNORED : MODE_ALLOWED);
-        } catch (Exception e) {
-            Slog.e(TAG, "Error while granting auto revoke exemption for " + packageName, e);
-        }
-    }
-
-    private <T> boolean containsEither(T[] array, T a, T b) {
-        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
-    }
-
-}
diff --git a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
index 1eb6d13f..60f4688 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
@@ -95,10 +95,7 @@
     /**
      * On package changed.
      */
-    public void onPackagesChanged(@UserIdInt int userId, String packageName) {
-        // TODO: We shouldn't need to clean up the whole user registry. We only need to remove the
-        //       package. I will do it in a separate change since it's not appropriate to use
-        //       PerUser anymore.
+    public void onPackagesChanged(@UserIdInt int userId) {
         mCompanionServicesRegister.invalidate(userId);
     }
 
diff --git a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
index 7b4dd7d..a374d27 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
@@ -57,7 +57,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.CollectionUtils;
-import com.android.server.companion.CompanionExemptionProcessor;
 import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
@@ -102,8 +101,6 @@
     private final PowerManagerInternal mPowerManagerInternal;
     @NonNull
     private final UserManager mUserManager;
-    @NonNull
-    private final CompanionExemptionProcessor mCompanionExemptionProcessor;
 
     // NOTE: Same association may appear in more than one of the following sets at the same time.
     // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
@@ -114,7 +111,7 @@
     @NonNull
     private final Set<Integer> mNearbyBleDevices = new HashSet<>();
     @NonNull
-    private final Set<Integer> mConnectedSelfManagedDevices = new HashSet<>();
+    private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
     @NonNull
     private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
     @NonNull
@@ -149,8 +146,7 @@
             @NonNull UserManager userManager,
             @NonNull AssociationStore associationStore,
             @NonNull ObservableUuidStore observableUuidStore,
-            @NonNull PowerManagerInternal powerManagerInternal,
-            @NonNull CompanionExemptionProcessor companionExemptionProcessor) {
+            @NonNull PowerManagerInternal powerManagerInternal) {
         mContext = context;
         mCompanionAppBinder = companionAppBinder;
         mAssociationStore = associationStore;
@@ -160,7 +156,6 @@
                 mObservableUuidStore, this);
         mBleDeviceProcessor = new BleDeviceProcessor(associationStore, this);
         mPowerManagerInternal = powerManagerInternal;
-        mCompanionExemptionProcessor = companionExemptionProcessor;
     }
 
     /** Initialize {@link DevicePresenceProcessor} */
@@ -409,7 +404,7 @@
      * nearby (for "self-managed" associations).
      */
     public boolean isDevicePresent(int associationId) {
-        return mConnectedSelfManagedDevices.contains(associationId)
+        return mReportedSelfManagedDevices.contains(associationId)
                 || mConnectedBtDevices.contains(associationId)
                 || mNearbyBleDevices.contains(associationId)
                 || mSimulated.contains(associationId);
@@ -456,7 +451,7 @@
      * notifyDeviceAppeared()}
      */
     public void onSelfManagedDeviceConnected(int associationId) {
-        onDevicePresenceEvent(mConnectedSelfManagedDevices,
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_APPEARED);
     }
 
@@ -472,7 +467,7 @@
      * notifyDeviceDisappeared()}
      */
     public void onSelfManagedDeviceDisconnected(int associationId) {
-        onDevicePresenceEvent(mConnectedSelfManagedDevices,
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
@@ -480,7 +475,7 @@
      * Marks a "self-managed" device as disconnected when binderDied.
      */
     public void onSelfManagedDeviceReporterBinderDied(int associationId) {
-        onDevicePresenceEvent(mConnectedSelfManagedDevices,
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
@@ -688,7 +683,6 @@
 
                 if (association.shouldBindWhenPresent()) {
                     bindApplicationIfNeeded(userId, packageName, association.isSelfManaged());
-                    mCompanionExemptionProcessor.exemptPackage(userId, packageName, true);
                 } else {
                     return;
                 }
@@ -721,7 +715,6 @@
                 // Check if there are other devices associated to the app that are present.
                 if (!shouldBindPackage(userId, packageName)) {
                     mCompanionAppBinder.unbindCompanionApp(userId, packageName);
-                    mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
                 }
                 break;
             default:
@@ -947,7 +940,7 @@
 
         mConnectedBtDevices.remove(id);
         mNearbyBleDevices.remove(id);
-        mConnectedSelfManagedDevices.remove(id);
+        mReportedSelfManagedDevices.remove(id);
         mSimulated.remove(id);
         synchronized (mBtDisconnectedDevices) {
             mBtDisconnectedDevices.remove(id);
@@ -1107,7 +1100,7 @@
         out.append("Companion Device Present: ");
         if (mConnectedBtDevices.isEmpty()
                 && mNearbyBleDevices.isEmpty()
-                && mConnectedSelfManagedDevices.isEmpty()) {
+                && mReportedSelfManagedDevices.isEmpty()) {
             out.append("<empty>\n");
             return;
         } else {
@@ -1137,11 +1130,11 @@
         }
 
         out.append("  Self-Reported Devices: ");
-        if (mConnectedSelfManagedDevices.isEmpty()) {
+        if (mReportedSelfManagedDevices.isEmpty()) {
             out.append("<empty>\n");
         } else {
             out.append("\n");
-            for (int associationId : mConnectedSelfManagedDevices) {
+            for (int associationId : mReportedSelfManagedDevices) {
                 AssociationInfo a = mAssociationStore.getAssociationById(associationId);
                 out.append("    ").append(a.toShortString()).append('\n');
             }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 348d83f..6cfd44b 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -126,6 +126,7 @@
         "platform_service_defaults",
         "android.hardware.power-java_shared",
         "latest_android_hardware_broadcastradio_java_static",
+        "services_crashrecovery_stubs_conditionally",
     ],
     srcs: [
         ":android.hardware.tv.hdmi.connection-V1-java-source",
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 51c768b..06e6c8b 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -48,6 +48,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.net.vcn.Flags;
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
@@ -524,6 +525,9 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
 
             switch (action) {
                 case Intent.ACTION_PACKAGE_ADDED: // Fallthrough
@@ -878,6 +882,7 @@
 
     private void garbageCollectAndWriteVcnConfigsLocked() {
         final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class);
+        final Set<ParcelUuid> subGroups = mLastSnapshot.getAllSubscriptionGroups();
 
         boolean shouldWrite = false;
 
@@ -885,11 +890,20 @@
         while (configsIterator.hasNext()) {
             final ParcelUuid subGrp = configsIterator.next();
 
-            final List<SubscriptionInfo> subscriptions = subMgr.getSubscriptionsInGroup(subGrp);
-            if (subscriptions == null || subscriptions.isEmpty()) {
-                // Trim subGrps with no more subscriptions; must have moved to another subGrp
-                configsIterator.remove();
-                shouldWrite = true;
+            if (Flags.fixConfigGarbageCollection()) {
+                if (!subGroups.contains(subGrp)) {
+                    // Trim subGrps with no more subscriptions; must have moved to another subGrp
+                    logDbg("Garbage collect VcnConfig for group=" + subGrp);
+                    configsIterator.remove();
+                    shouldWrite = true;
+                }
+            } else {
+                final List<SubscriptionInfo> subscriptions = subMgr.getSubscriptionsInGroup(subGrp);
+                if (subscriptions == null || subscriptions.isEmpty()) {
+                    // Trim subGrps with no more subscriptions; must have moved to another subGrp
+                    configsIterator.remove();
+                    shouldWrite = true;
+                }
             }
         }
 
@@ -1094,13 +1108,7 @@
             synchronized (mLock) {
                 final Vcn vcn = mVcns.get(subGrp);
                 final VcnConfig vcnConfig = mConfigs.get(subGrp);
-                if (vcn != null) {
-                    if (vcnConfig == null) {
-                        // TODO: b/284381334 Investigate for the root cause of this issue
-                        // and handle it properly
-                        logWtf("Vcn instance exists but VcnConfig does not for " + subGrp);
-                    }
-
+                if (vcn != null && vcnConfig != null) {
                     if (vcn.getStatus() == VCN_STATUS_CODE_ACTIVE) {
                         isVcnManagedNetwork = true;
                     }
@@ -1120,6 +1128,8 @@
                             }
                         }
                     }
+                } else if (vcn != null && vcnConfig == null) {
+                    logWtf("Vcn instance exists but VcnConfig does not for " + subGrp);
                 }
             }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2a9cb43..1c3569d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -60,7 +60,6 @@
 import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_INSTRUMENTATION;
 import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_PERSISTENT;
 import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_SYSTEM;
-import static android.content.Intent.isPreventIntentRedirectEnabled;
 import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
 import static android.content.pm.PackageManager.GET_SHARED_LIBRARY_FILES;
 import static android.content.pm.PackageManager.MATCH_ALL;
@@ -131,6 +130,7 @@
 import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES;
 import static android.provider.Settings.Global.DEBUG_APP;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
+import static android.security.Flags.preventIntentRedirect;
 import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -14301,6 +14301,10 @@
         mBroadcastController.unregisterReceiver(receiver);
     }
 
+    public List<IntentFilter> getRegisteredIntentFilters(IIntentReceiver receiver) {
+        return mBroadcastController.getRegisteredIntentFilters(receiver);
+    }
+
     @GuardedBy("this")
     final int broadcastIntentLocked(ProcessRecord callerApp,
             String callerPackage, String callerFeatureId, Intent intent, String resolvedType,
@@ -19281,7 +19285,7 @@
      * @hide
      */
     public void addCreatorToken(@Nullable Intent intent, String creatorPackage) {
-        if (!isPreventIntentRedirectEnabled()) return;
+        if (!preventIntentRedirect()) return;
 
         if (intent == null || intent.getExtraIntentKeys() == null) return;
         for (String key : intent.getExtraIntentKeys()) {
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index 71ddf05..b0f88071 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -679,6 +679,21 @@
         }
     }
 
+    List<IntentFilter> getRegisteredIntentFilters(IIntentReceiver receiver) {
+        synchronized (mService) {
+            final ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
+            if (rl == null) {
+                return null;
+            }
+            final ArrayList<IntentFilter> filters = new ArrayList<>();
+            final int count = rl.size();
+            for (int i = 0; i < count; ++i) {
+                filters.add(rl.get(i));
+            }
+            return filters;
+        }
+    }
+
     int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,
             Intent intent, String resolvedType, IIntentReceiver resultTo,
             int resultCode, String resultData, Bundle resultExtras,
diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java
index f016590..3c7fb52 100644
--- a/services/core/java/com/android/server/am/BroadcastFilter.java
+++ b/services/core/java/com/android/server/am/BroadcastFilter.java
@@ -19,7 +19,6 @@
 import android.annotation.Nullable;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
-import android.compat.annotation.Overridable;
 import android.content.IntentFilter;
 import android.os.UserHandle;
 import android.util.PrintWriterPrinter;
@@ -40,7 +39,6 @@
      */
     @ChangeId
     @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
-    @Overridable
     @VisibleForTesting
     static final long CHANGE_RESTRICT_PRIORITY_VALUES = 371309185L;
 
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7cfe829..bcde1ff 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -209,6 +209,7 @@
         "pixel_perf",
         "pixel_sensors",
         "pixel_system_sw_video",
+        "pixel_video_sw",
         "pixel_watch",
         "platform_compat",
         "platform_security",
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 03afaea..702ad95 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -184,6 +184,7 @@
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageState;
 import com.android.server.policy.AppOpsPolicy;
+import com.android.server.selinux.RateLimiter;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -203,6 +204,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -353,6 +355,10 @@
     @GuardedBy("this")
     private boolean mUidStatesInitialized;
 
+    // A rate limiter to prevent excessive Atom pushing. Used by noteOperation.
+    private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10);
+    private final RateLimiter mRateLimiter = new RateLimiter(RATE_LIMITER_WINDOW);
+
     volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this);
 
     /*
@@ -3137,10 +3143,12 @@
             boolean shouldCollectMessage) {
         if (Binder.getCallingPid() != Process.myPid()
                 && Flags.appopAccessTrackingLoggingEnabled()) {
-            FrameworkStatsLog.write(
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
-                    attributionTag != null);
+            if (mRateLimiter.tryAcquire()) {
+                FrameworkStatsLog.write(
+                        APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+                        APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
+                        attributionTag != null);
+            }
         }
         return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
                 attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 906e584..a3b20b9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -693,6 +693,8 @@
                 elapsed = System.currentTimeMillis() - start;
                 if (elapsed >= SET_COMMUNICATION_DEVICE_TIMEOUT_MS) {
                     Log.e(TAG, "Timeout waiting for communication device update.");
+                    // reset counter to avoid sticky out of sync condition
+                    mCommunicationDeviceUpdateCount = 0;
                     break;
                 }
             }
@@ -1342,8 +1344,8 @@
     }
 
     /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) {
-        sendILMsgNoDelay(MSG_IL_SET_MODE_OWNER, SENDMSG_REPLACE,
-                signal ? 1 : 0, new AudioModeInfo(mode, pid, uid));
+        sendLMsgNoDelay(signal ? MSG_L_SET_MODE_OWNER_SIGNAL : MSG_L_SET_MODE_OWNER,
+                SENDMSG_REPLACE, new AudioModeInfo(mode, pid, uid));
     }
 
     /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) {
@@ -2026,7 +2028,8 @@
                         mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
                     }
                     break;
-                case MSG_IL_SET_MODE_OWNER:
+                case MSG_L_SET_MODE_OWNER:
+                case MSG_L_SET_MODE_OWNER_SIGNAL:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
                             mAudioModeOwner = (AudioModeInfo) msg.obj;
@@ -2037,7 +2040,7 @@
                             }
                         }
                     }
-                    if (msg.arg1 == 1 /*signal*/) {
+                    if (msg.what == MSG_L_SET_MODE_OWNER_SIGNAL) {
                         mAudioService.decrementAudioModeResetCount();
                     }
                     break;
@@ -2199,7 +2202,8 @@
     private static final int MSG_REPORT_NEW_ROUTES = 13;
     private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
     private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
-    private static final int MSG_IL_SET_MODE_OWNER = 16;
+    private static final int MSG_L_SET_MODE_OWNER = 16;
+    private static final int MSG_L_SET_MODE_OWNER_SIGNAL = 17;
 
     private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22;
     private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3dbe48a..985155d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -457,7 +457,7 @@
     private static final int MSG_UPDATE_AUDIO_MODE = 36;
     private static final int MSG_RECORDING_CONFIG_CHANGE = 37;
     private static final int MSG_BT_DEV_CHANGED = 38;
-
+    private static final int MSG_UPDATE_AUDIO_MODE_SIGNAL = 39;
     private static final int MSG_DISPATCH_AUDIO_MODE = 40;
     private static final int MSG_ROUTING_UPDATED = 41;
     private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
@@ -4804,16 +4804,14 @@
     }
 
     static class UpdateAudioModeInfo {
-        UpdateAudioModeInfo(int mode, int pid, String packageName, boolean signal) {
+        UpdateAudioModeInfo(int mode, int pid, String packageName) {
             mMode = mode;
             mPid = pid;
             mPackageName = packageName;
-            mSignal = signal;
         }
         private final int mMode;
         private final int mPid;
         private final String mPackageName;
-        private final boolean mSignal;
 
         int getMode() {
             return mMode;
@@ -4824,9 +4822,6 @@
         String getPackageName() {
             return mPackageName;
         }
-        boolean getSignal() {
-            return mSignal;
-        }
     }
 
     void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName,
@@ -4835,8 +4830,8 @@
             if (signal) {
                 mAudioModeResetCount++;
             }
-            sendMsg(mAudioHandler, MSG_UPDATE_AUDIO_MODE, msgPolicy, 0, 0,
-                new UpdateAudioModeInfo(mode, pid, packageName, signal), delay);
+            sendMsg(mAudioHandler, signal ? MSG_UPDATE_AUDIO_MODE_SIGNAL : MSG_UPDATE_AUDIO_MODE,
+                    msgPolicy, 0, 0, new UpdateAudioModeInfo(mode, pid, packageName), delay);
         }
     }
 
@@ -6654,6 +6649,9 @@
                 // connections not started by the application changing the mode when pid changes
                 mDeviceBroker.postSetModeOwner(mode, pid, uid, signal);
             } else {
+                // reset here to avoid sticky out of sync condition (would have been reset
+                // by AudioDeviceBroker processing MSG_L_SET_MODE_OWNER_SIGNAL message)
+                resetAudioModeResetCount();
                 Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode);
             }
         }
@@ -10420,10 +10418,11 @@
                     break;
 
                 case MSG_UPDATE_AUDIO_MODE:
+                case MSG_UPDATE_AUDIO_MODE_SIGNAL:
                     synchronized (mDeviceBroker.mSetModeLock) {
                         UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj;
                         onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(),
-                                false /*force*/, info.getSignal());
+                                false /*force*/, msg.what == MSG_UPDATE_AUDIO_MODE_SIGNAL);
                     }
                     break;
 
@@ -11164,6 +11163,8 @@
                     elapsed = java.lang.System.currentTimeMillis() - start;
                     if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) {
                         Log.e(TAG, "Timeout waiting for audio mode reset");
+                        // reset count to avoid sticky out of sync state.
+                        resetAudioModeResetCount();
                         break;
                     }
                 }
@@ -11175,7 +11176,7 @@
         return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
     }
 
-    /** synchronization between setMode(NORMAL) and abandonAudioFocus() frmo Telecom */
+    /** synchronization between setMode(NORMAL) and abandonAudioFocus() from Telecom */
     private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000;
 
     private final Object mAudioModeResetLock = new Object();
@@ -11194,6 +11195,13 @@
         }
     }
 
+    private void resetAudioModeResetCount() {
+        synchronized (mAudioModeResetLock) {
+            mAudioModeResetCount = 0;
+            mAudioModeResetLock.notify();
+        }
+    }
+
     /** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */
     public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId,
             AudioAttributes aa, String callingPackageName) {
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 1da62d7..1604e94 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -1068,6 +1068,7 @@
         switch (attr.getUsage()) {
             case AudioAttributes.USAGE_MEDIA:
             case AudioAttributes.USAGE_GAME:
+            case AudioAttributes.USAGE_SPEAKER_CLEANUP:
                 return 1000;
             case AudioAttributes.USAGE_ALARM:
             case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index 3afecf1..290aab2 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -305,7 +305,7 @@
                     mSensors /* sensorIds */,
                     true /* credentialAllowed */,
                     false /* requireConfirmation */,
-                    mUserId,
+                    mPreAuthInfo.callingUserId,
                     mOperationId,
                     mOpPackageName,
                     mRequestId);
@@ -357,7 +357,7 @@
                             mSensors,
                             mPreAuthInfo.shouldShowCredential(),
                             requireConfirmation,
-                            mUserId,
+                            mPreAuthInfo.callingUserId,
                             mOperationId,
                             mOpPackageName,
                             mRequestId);
@@ -491,7 +491,7 @@
                             mSensors /* sensorIds */,
                             true /* credentialAllowed */,
                             false /* requireConfirmation */,
-                            mUserId,
+                            mPreAuthInfo.callingUserId,
                             mOperationId,
                             mOpPackageName,
                             mRequestId);
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 4c91789..00280c8f 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -1133,7 +1133,7 @@
 
         return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors,
                 userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */,
-                getContext(), mBiometricCameraManager);
+                getContext(), mBiometricCameraManager, mUserManager);
     }
 
     /**
@@ -1520,9 +1520,9 @@
         mHandler.post(() -> {
             try {
                 final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager,
-                        mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo,
-                        opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
-                        getContext(), mBiometricCameraManager);
+                        mDevicePolicyManager, mSettingObserver, mSensors, userId,
+                        promptInfo, opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
+                        getContext(), mBiometricCameraManager, mUserManager);
 
                 // Set the default title if necessary.
                 if (promptInfo.isUseDefaultTitle()) {
@@ -1572,8 +1572,8 @@
                         promptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
                     }
 
-                    authenticateInternal(token, requestId, operationId, userId, receiver,
-                            opPackageName, promptInfo, preAuthInfo);
+                    authenticateInternal(token, requestId, operationId, preAuthInfo.userId,
+                            receiver, opPackageName, promptInfo, preAuthInfo);
                 } else {
                     receiver.onError(preAuthStatus.first /* modality */,
                             preAuthStatus.second /* errorCode */,
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index 96c178a..a9ada29 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -32,6 +32,7 @@
 import android.hardware.biometrics.Flags;
 import android.hardware.biometrics.PromptInfo;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.Pair;
 import android.util.Slog;
 
@@ -72,6 +73,7 @@
     final boolean confirmationRequested;
     final boolean ignoreEnrollmentState;
     final int userId;
+    final int callingUserId;
     final Context context;
     private final boolean mBiometricRequested;
     private final int mBiometricStrengthRequested;
@@ -82,7 +84,7 @@
     private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
             boolean credentialRequested, List<BiometricSensor> eligibleSensors,
             List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
-            PromptInfo promptInfo, int userId, Context context,
+            PromptInfo promptInfo, int userId, int callingUserId, Context context,
             BiometricCameraManager biometricCameraManager,
             boolean isOnlyMandatoryBiometricsRequested,
             boolean isMandatoryBiometricsAuthentication) {
@@ -97,6 +99,7 @@
         this.confirmationRequested = promptInfo.isConfirmationRequested();
         this.ignoreEnrollmentState = promptInfo.isIgnoreEnrollmentState();
         this.userId = userId;
+        this.callingUserId = callingUserId;
         this.context = context;
         this.mOnlyMandatoryBiometricsRequested = isOnlyMandatoryBiometricsRequested;
         this.mIsMandatoryBiometricsAuthentication = isMandatoryBiometricsAuthentication;
@@ -108,7 +111,8 @@
             List<BiometricSensor> sensors,
             int userId, PromptInfo promptInfo, String opPackageName,
             boolean checkDevicePolicyManager, Context context,
-            BiometricCameraManager biometricCameraManager)
+            BiometricCameraManager biometricCameraManager,
+            UserManager userManager)
             throws RemoteException {
 
         final boolean isOnlyMandatoryBiometricsRequested = promptInfo.getAuthenticators()
@@ -141,14 +145,20 @@
         final List<BiometricSensor> eligibleSensors = new ArrayList<>();
         final List<Pair<BiometricSensor, Integer>> ineligibleSensors = new ArrayList<>();
 
+        final int effectiveUserId;
+        if (Flags.privateSpaceBp()) {
+            effectiveUserId = userManager.getCredentialOwnerProfile(userId);
+        } else {
+            effectiveUserId = userId;
+        }
+
         if (biometricRequested) {
             for (BiometricSensor sensor : sensors) {
 
                 @AuthenticatorStatus int status = getStatusForBiometricAuthenticator(
-                        devicePolicyManager, settingObserver, sensor, userId, opPackageName,
-                        checkDevicePolicyManager, requestedStrength,
-                        promptInfo.getAllowedSensorIds(),
-                        promptInfo.isIgnoreEnrollmentState(),
+                        devicePolicyManager, settingObserver, sensor, effectiveUserId,
+                        opPackageName, checkDevicePolicyManager, requestedStrength,
+                        promptInfo.getAllowedSensorIds(), promptInfo.isIgnoreEnrollmentState(),
                         biometricCameraManager);
 
                 Slog.d(TAG, "Package: " + opPackageName
@@ -172,9 +182,9 @@
         }
 
         return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested,
-                eligibleSensors, ineligibleSensors, credentialAvailable, promptInfo, userId,
-                context, biometricCameraManager, isOnlyMandatoryBiometricsRequested,
-                isMandatoryBiometricsAuthentication);
+                eligibleSensors, ineligibleSensors, credentialAvailable, promptInfo,
+                effectiveUserId, userId, context, biometricCameraManager,
+                isOnlyMandatoryBiometricsRequested, isMandatoryBiometricsAuthentication);
     }
 
     private static boolean dropCredentialFallback(int authenticators,
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3603cdb..f5a75c7d 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -25,7 +25,7 @@
 import static android.Manifest.permission.RESTRICT_DISPLAY_MODES;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
-import static android.hardware.display.DisplayManager.EventsMask;
+import static android.hardware.display.DisplayManager.EventFlag;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
@@ -1390,16 +1390,16 @@
     }
 
     private void registerCallbackInternal(IDisplayManagerCallback callback, int callingPid,
-            int callingUid, @EventsMask long eventsMask) {
+            int callingUid, @EventFlag long eventFlagsMask) {
         synchronized (mSyncRoot) {
             CallbackRecord record = mCallbacks.get(callingPid);
 
             if (record != null) {
-                record.updateEventsMask(eventsMask);
+                record.updateEventFlagsMask(eventFlagsMask);
                 return;
             }
 
-            record = new CallbackRecord(callingPid, callingUid, callback, eventsMask);
+            record = new CallbackRecord(callingPid, callingUid, callback, eventFlagsMask);
             try {
                 IBinder binder = callback.asBinder();
                 binder.linkToDeath(record, 0);
@@ -1889,6 +1889,7 @@
             final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
                     packageName, callingUid, virtualDisplayConfig);
 
+            boolean shouldClearDisplayWindowSettings = false;
             if (virtualDisplayConfig.isHomeSupported()) {
                 if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) {
                     Slog.w(TAG, "Display created with home support but lacks "
@@ -1900,6 +1901,18 @@
                 } else {
                     mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId,
                             Display.TYPE_VIRTUAL, true);
+                    shouldClearDisplayWindowSettings = true;
+                }
+            }
+
+            if (virtualDisplayConfig.isIgnoreActivitySizeRestrictions()) {
+                if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) {
+                    Slog.w(TAG, "Display created to ignore activity size restrictions, "
+                            + "but lacks VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the request.");
+                } else {
+                    mWindowManagerInternal.setIgnoreActivitySizeRestrictionsOnDisplay(
+                            displayUniqueId, Display.TYPE_VIRTUAL, true);
+                    shouldClearDisplayWindowSettings = true;
                 }
             }
 
@@ -1922,8 +1935,7 @@
                 }
             }
 
-            if (displayId == Display.INVALID_DISPLAY && virtualDisplayConfig.isHomeSupported()
-                    && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0) {
+            if (displayId == Display.INVALID_DISPLAY && shouldClearDisplayWindowSettings) {
                 // Failed to create the virtual display, so we should clean up the WM settings
                 // because it won't receive the onDisplayRemoved callback.
                 mWindowManagerInternal.clearDisplaySettings(displayUniqueId, Display.TYPE_VIRTUAL);
@@ -3997,7 +4009,7 @@
         public final int mPid;
         public final int mUid;
         private final IDisplayManagerCallback mCallback;
-        private @EventsMask AtomicLong mEventsMask;
+        private @DisplayManager.EventFlag AtomicLong mEventFlagsMask;
         private final String mPackageName;
 
         public boolean mWifiDisplayScanRequested;
@@ -4018,11 +4030,11 @@
         private boolean mFrozen;
 
         CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback,
-                @EventsMask long eventsMask) {
+                @EventFlag long eventFlagsMask) {
             mPid = pid;
             mUid = uid;
             mCallback = callback;
-            mEventsMask = new AtomicLong(eventsMask);
+            mEventFlagsMask = new AtomicLong(eventFlagsMask);
             mCached = false;
             mFrozen = false;
 
@@ -4044,8 +4056,8 @@
             mPackageName = packageNames == null ? null : packageNames[0];
         }
 
-        public void updateEventsMask(@EventsMask long eventsMask) {
-            mEventsMask.set(eventsMask);
+        public void updateEventFlagsMask(@EventFlag long eventFlag) {
+            mEventFlagsMask.set(eventFlag);
         }
 
         /**
@@ -4109,12 +4121,13 @@
             if (!shouldSendEvent(event)) {
                 if (extraLogging(mPackageName)) {
                     Slog.i(TAG,
-                            "Not sending displayEvent: " + event + " due to mask:" + mEventsMask);
+                            "Not sending displayEvent: " + event + " due to flag:"
+                                    + mEventFlagsMask);
                 }
                 if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
                     Trace.instant(Trace.TRACE_TAG_POWER,
-                            "notifyDisplayEventAsync#notSendingEvent=" + event + ",mEventsMask="
-                                    + mEventsMask);
+                            "notifyDisplayEventAsync#notSendingEvent=" + event + ",mEventsFlag="
+                                    + mEventFlagsMask);
                 }
                 // The client is not interested in this event, so do nothing.
                 return true;
@@ -4160,22 +4173,22 @@
          * Return true if the client is interested in this event.
          */
         private boolean shouldSendEvent(@DisplayEvent int event) {
-            final long mask = mEventsMask.get();
+            final long flag = mEventFlagsMask.get();
             switch (event) {
                 case DisplayManagerGlobal.EVENT_DISPLAY_ADDED:
-                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_CHANGED:
-                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED:
-                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_REMOVED:
-                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
-                    return (mask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0;
                 case DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED:
                     // fallthrough
                 case DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED:
-                    return (mask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0;
+                    return (flag & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0;
                 default:
                     // This should never happen.
                     Slog.e(TAG, "Unknown display event " + event);
@@ -4369,7 +4382,7 @@
         @Override // Binder call
         @SuppressLint("AndroidFrameworkRequiresPermission") // Permission only required sometimes
         public void registerCallbackWithEventMask(IDisplayManagerCallback callback,
-                @EventsMask long eventsMask) {
+                @EventFlag long eventFlagsMask) {
             if (callback == null) {
                 throw new IllegalArgumentException("listener must not be null");
             }
@@ -4378,7 +4391,7 @@
             final int callingUid = Binder.getCallingUid();
 
             if (mFlags.isConnectedDisplayManagementEnabled()) {
-                if ((eventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                if ((eventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
                     mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS,
                             "Permission required to get signals about connection events.");
                 }
@@ -4386,7 +4399,7 @@
 
             final long token = Binder.clearCallingIdentity();
             try {
-                registerCallbackInternal(callback, callingPid, callingUid, eventsMask);
+                registerCallbackInternal(callback, callingPid, callingUid, eventFlagsMask);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 0766c3a..132d6fa 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -3178,6 +3178,10 @@
         HdmiCecLocalDeviceSource source = playback();
         if (source == null) {
             source = audioSystem();
+        } else {
+            // Cancel an existing timer to send the device to sleep since OTP was triggered.
+            playback().mDelayedStandbyOnActiveSourceLostHandler
+                    .removeCallbacksAndMessages(null);
         }
 
         if (source == null) {
diff --git a/services/core/java/com/android/server/input/InputShellCommand.java b/services/core/java/com/android/server/input/InputShellCommand.java
index 4c5a3c2..9833016 100644
--- a/services/core/java/com/android/server/input/InputShellCommand.java
+++ b/services/core/java/com/android/server/input/InputShellCommand.java
@@ -89,6 +89,9 @@
     private static final int DEFAULT_FLAGS = 0;
     private static final boolean INJECT_ASYNC = true;
     private static final boolean INJECT_SYNC = false;
+    private static final long SECOND_IN_MILLISECONDS = 1000;
+
+    public static final int SWIPE_EVENT_HZ_DEFAULT = 120;
 
     /** Modifier key to meta state */
     private static final Map<Integer, Integer> MODIFIER;
@@ -519,11 +522,30 @@
         }
         long now = SystemClock.uptimeMillis();
         final long endTime = down + duration;
+        final float swipeEventPeriodMillis =
+                (float) SECOND_IN_MILLISECONDS / SWIPE_EVENT_HZ_DEFAULT;
+        int injected = 1;
         while (now < endTime) {
-            final long elapsedTime = now - down;
+            // Ensure that we inject at most at the frequency of SWIPE_EVENT_HZ_DEFAULT
+            // by waiting an additional delta between the actual time and expected time.
+            long elapsedTime = now - down;
+            final long errorMillis =
+                    (long) Math.floor(injected * swipeEventPeriodMillis - elapsedTime);
+            if (errorMillis > 0) {
+                // Make sure not to exceed the duration and inject an extra event.
+                if (errorMillis > endTime - now) {
+                    sleep(endTime - now);
+                    break;
+                }
+                sleep(errorMillis);
+            }
+
+            now = SystemClock.uptimeMillis();
+            elapsedTime = now - down;
             final float alpha = (float) elapsedTime / duration;
             injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, down, now,
                     lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId);
+            injected++;
             now = SystemClock.uptimeMillis();
         }
         injectMotionEvent(inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f,
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 68eaf72..e665f96 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -25,6 +25,7 @@
 import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
 import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut;
+import static com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts;
 
 import android.annotation.BinderThread;
 import android.annotation.MainThread;
@@ -734,6 +735,54 @@
                     }
                 }
                 break;
+            case KeyEvent.KEYCODE_LEFT_BRACKET:
+                if (enableTaskResizingKeyboardShortcuts()) {
+                    if (firstDown && event.isAltPressed()) {
+                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                                KeyEvent.META_ALT_ON,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                displayId,
+                                focusedToken, /* flags = */0);
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_RIGHT_BRACKET:
+                if (enableTaskResizingKeyboardShortcuts()) {
+                    if (firstDown && event.isAltPressed()) {
+                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                                KeyEvent.META_ALT_ON,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                displayId,
+                                focusedToken, /* flags = */0);
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_EQUALS:
+                if (enableTaskResizingKeyboardShortcuts()) {
+                    if (firstDown && event.isAltPressed()) {
+                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                                KeyEvent.META_ALT_ON,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                displayId,
+                                focusedToken, /* flags = */0);
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_MINUS:
+                if (enableTaskResizingKeyboardShortcuts()) {
+                    if (firstDown && event.isAltPressed()) {
+                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                                KeyEvent.META_ALT_ON,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                                displayId,
+                                focusedToken, /* flags = */0);
+                    }
+                }
+                break;
             case KeyEvent.KEYCODE_SLASH:
                 if (firstDown && event.isMetaPressed()) {
                     return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON,
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index bbdac56..45885f0 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -253,10 +253,10 @@
 
     private static final String MIGRATED_FRP2 = "migrated_frp2";
     private static final String MIGRATED_KEYSTORE_NS = "migrated_keystore_namespace";
-    private static final String MIGRATED_SP_CE_ONLY = "migrated_all_users_to_sp_and_bound_ce";
     private static final String MIGRATED_SP_FULL = "migrated_all_users_to_sp_and_bound_keys";
     private static final String MIGRATED_WEAVER_DISABLED_ON_UNSECURED_USERS =
             "migrated_weaver_disabled_on_unsecured_users";
+    // Note: some other migrated_* strings used to be used and may exist in the database already.
 
     // Duration that LockSettingsService will store the gatekeeper password for. This allows
     // multiple biometric enrollments without prompting the user to enter their password via
@@ -1183,9 +1183,7 @@
 
             // If config_disableWeaverOnUnsecuredUsers=true, then the Weaver HAL may be buggy and
             // need multiple retries before it works here to unwrap the SP, if the SP was already
-            // protected by Weaver.  Note that the problematic HAL can also deadlock if called with
-            // the ActivityManagerService lock held, but that should not be a problem here since
-            // that lock isn't held here, unlike unlockUserKeyIfUnsecured() where it is.
+            // protected by Weaver.
             for (int i = 0; i < 12 && sp == null; i++) {
                 Slog.e(TAG, "Failed to unwrap synthetic password. Waiting 5 seconds to retry.");
                 SystemClock.sleep(5000);
@@ -1221,21 +1219,16 @@
                 Slog.i(TAG, "Synthetic password is already not protected by Weaver");
             }
         } else if (sp == null) {
-            Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
-            return;
+            throw new IllegalStateException(
+                    "Failed to unwrap synthetic password for unsecured user " + userId);
         }
 
         // Call setCeStorageProtection(), to re-encrypt the CE key with the SP if it's currently
-        // encrypted by an empty secret.  Skip this if it was definitely already done as part of the
-        // upgrade to Android 14, since while setCeStorageProtection() is idempotent it does log
-        // some error messages when called again.  Do not skip this if
-        // config_disableWeaverOnUnsecuredUsers=true, since in that case we'd like to recover from
-        // the case where an earlier upgrade to Android 14 incorrectly skipped this step.
-        if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null
-                || isWeaverDisabledOnUnsecuredUsers()) {
-            Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId);
-            setCeStorageProtection(userId, sp);
-        }
+        // encrypted by an empty secret.  If the CE key is already encrypted by the SP, then this is
+        // a no-op except for some log messages.
+        Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId);
+        setCeStorageProtection(userId, sp);
+
         Slogf.i(TAG, "Initializing Keystore super keys for user %d", userId);
         initKeystoreSuperKeys(userId, sp, /* allowExisting= */ true);
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 62df825..849f236 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -105,6 +105,7 @@
 import static android.service.notification.Adjustment.KEY_TYPE;
 import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION;
 import static android.service.notification.Adjustment.TYPE_PROMOTION;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT;
 import static android.service.notification.Flags.callstyleCallbackApi;
 import static android.service.notification.Flags.notificationClassification;
 import static android.service.notification.Flags.notificationForceGrouping;
@@ -6681,6 +6682,33 @@
         }
 
         @Override
+        @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+        public NotificationChannel createConversationNotificationChannelForPackageFromPrivilegedListener(
+                INotificationListener token, String pkg, UserHandle user,
+                String parentId, String conversationId) throws RemoteException {
+            Objects.requireNonNull(pkg);
+            Objects.requireNonNull(user);
+            Objects.requireNonNull(parentId);
+            Objects.requireNonNull(conversationId);
+
+            verifyPrivilegedListener(token, user, true);
+
+            int uid = getUidForPackageAndUser(pkg, user);
+            NotificationChannel conversationChannel =
+                    mPreferencesHelper.getNotificationChannel(pkg, uid, parentId, false).copy();
+            String conversationChannelId = String.format(
+                    CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId);
+            conversationChannel.setId(conversationChannelId);
+            conversationChannel.setConversationId(parentId, conversationId);
+            createNotificationChannelsImpl(
+                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)));
+            handleSavePolicyFile();
+
+            return mPreferencesHelper.getConversationNotificationChannel(
+                    pkg, uid, parentId, conversationId, false, false).copy();
+        }
+
+        @Override
         public void updateNotificationChannelGroupFromPrivilegedListener(
                 INotificationListener token, String pkg, UserHandle user,
                 NotificationChannelGroup group) throws RemoteException {
@@ -6698,7 +6726,7 @@
             Objects.requireNonNull(pkg);
             Objects.requireNonNull(user);
 
-            verifyPrivilegedListener(token, user, false);
+            verifyPrivilegedListener(token, user, true);
 
             final NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel(
                     pkg, getUidForPackageAndUser(pkg, user), channel.getId(), true);
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index ce249c6..d5f13a8 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -40,6 +40,7 @@
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
 import static android.service.notification.ZenModeConfig.implicitRuleId;
+import static android.service.notification.ZenModeConfig.isImplicitRuleId;
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 import static com.android.internal.util.Preconditions.checkArgument;
@@ -170,7 +171,6 @@
     private final Clock mClock;
     private final SettingsObserver mSettingsObserver;
     private final AppOpsManager mAppOps;
-    private final NotificationManager mNotificationManager;
     private final ZenModeConfig mDefaultConfig;
     private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
     private final ZenModeFiltering mFiltering;
@@ -217,7 +217,6 @@
         mClock = clock;
         addCallback(mMetrics);
         mAppOps = context.getSystemService(AppOpsManager.class);
-        mNotificationManager = context.getSystemService(NotificationManager.class);
 
         mDefaultConfig = Flags.modesUi()
                 ? ZenModeConfig.getDefaultConfig()
@@ -660,7 +659,12 @@
                     // (whether initialized here or set via app or user).
                     rule.zenPolicy = config.getZenPolicy().copy();
                     newConfig.automaticRules.put(rule.id, rule);
+                } else {
+                    if (Flags.modesUi()) {
+                        updateImplicitZenRuleNameAndDescription(rule);
+                    }
                 }
+
                 // If the user has changed the rule's *zenMode*, then don't let app overwrite it.
                 // We allow the update if the user has only changed other aspects of the rule.
                 if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) {
@@ -707,7 +711,12 @@
                 rule = newImplicitZenRule(callingPkg);
                 rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
                 newConfig.automaticRules.put(rule.id, rule);
+            } else {
+                if (Flags.modesUi()) {
+                    updateImplicitZenRuleNameAndDescription(rule);
+                }
             }
+
             // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it.
             // We allow the update if the user has only changed other aspects of the rule.
             if (rule.zenPolicyUserModifiedFields == 0) {
@@ -777,24 +786,8 @@
         rule.id = implicitRuleId(pkg);
         rule.pkg = pkg;
         rule.creationTime = mClock.millis();
-
-        Binder.withCleanCallingIdentity(() -> {
-            try {
-                ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0);
-                rule.name = applicationInfo.loadLabel(mPm).toString();
-                if (!Flags.modesUi()) {
-                    rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon);
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                // Should not happen, since it's the app calling us (?)
-                Log.w(TAG, "Package not found for creating implicit zen rule");
-                rule.name = "Unknown";
-            }
-        });
-
+        updateImplicitZenRuleNameAndDescription(rule);
         rule.type = AutomaticZenRule.TYPE_OTHER;
-        rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description,
-                rule.name);
         rule.condition = null;
         rule.conditionId = new Uri.Builder()
                 .scheme(Condition.SCHEME)
@@ -809,6 +802,38 @@
         return rule;
     }
 
+    private void updateImplicitZenRuleNameAndDescription(ZenRule rule) {
+        checkArgument(isImplicitRuleId(rule.id));
+        requireNonNull(rule.pkg, "Implicit rule is not associated to package yet!");
+
+        String pkgAppName = Binder.withCleanCallingIdentity(() -> {
+            try {
+                ApplicationInfo applicationInfo = mPm.getApplicationInfo(rule.pkg, 0);
+                return applicationInfo.loadLabel(mPm).toString();
+            } catch (PackageManager.NameNotFoundException e) {
+                // Should not happen. When creating it's the app calling us, and when updating
+                // the rule would've been deleted if the package was removed.
+                Slog.e(TAG, "Package not found when updating implicit zen rule name", e);
+                return null;
+            }
+        });
+
+        if (pkgAppName != null) {
+            if ((rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
+                if (Flags.modesUi()) {
+                    rule.name = mContext.getString(R.string.zen_mode_implicit_name, pkgAppName);
+                } else {
+                    rule.name = pkgAppName;
+                }
+            }
+            rule.triggerDescription = mContext.getString(
+                    R.string.zen_mode_implicit_trigger_description, pkgAppName);
+        } else if (rule.name == null) {
+            // We must give a new rule SOME name. But this path should never be hit.
+            rule.name = "Unknown";
+        }
+    }
+
     boolean removeAutomaticZenRule(UserHandle user, String id, @ConfigOrigin int origin,
             String reason, int callingUid) {
         checkManageRuleOrigin("removeAutomaticZenRule", origin);
@@ -1130,6 +1155,8 @@
                 for (ZenRule rule : newConfig.automaticRules.values()) {
                     if (SystemZenRules.isSystemOwnedRule(rule)) {
                         updated |= SystemZenRules.updateTriggerDescription(mContext, rule);
+                    } else if (isImplicitRuleId(rule.id)) {
+                        updateImplicitZenRuleNameAndDescription(rule);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
index 1553618..27c4e9d 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
@@ -35,6 +35,7 @@
 
     @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName";
     @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId";
+    @VisibleForTesting static final String INSTALL_EVENT_TYPE_KEY = "installEventType";
     private static final String TAG = "BackgroundInstallControlCallbackHelper";
 
     private final Handler mHandler;
@@ -74,10 +75,14 @@
      * Invokes all registered callbacks Callbacks are processed through user provided-threads and
      * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent
      */
-    public void notifyAllCallbacks(int userId, String packageName) {
+    public void notifyAllCallbacks(
+            int userId,
+            String packageName,
+            @BackgroundInstallControlService.InstallEventType int installEventType) {
         Bundle extras = new Bundle();
         extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName);
         extras.putInt(FLAGGED_USER_ID_KEY, userId);
+        extras.putInt(INSTALL_EVENT_TYPE_KEY, installEventType);
         synchronized (mCallbacks) {
             mHandler.post(
                     () ->
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index b6daed1..20cca969 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.app.Flags;
@@ -64,6 +65,8 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
@@ -76,11 +79,23 @@
  * @hide
  */
 public class BackgroundInstallControlService extends SystemService {
+    public static final int INSTALL_EVENT_TYPE_UNKNOWN = 0;
+    public static final int INSTALL_EVENT_TYPE_INSTALL = 1;
+    public static final int INSTALL_EVENT_TYPE_UNINSTALL = 2;
+
+    @IntDef(
+            value = {
+                INSTALL_EVENT_TYPE_UNKNOWN,
+                INSTALL_EVENT_TYPE_INSTALL,
+                INSTALL_EVENT_TYPE_UNINSTALL,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InstallEventType {}
+
     private static final String TAG = "BackgroundInstallControlService";
 
     private static final String DISK_FILE_NAME = "states";
     private static final String DISK_DIR_NAME = "bic";
-
     private static final String ENFORCE_PERMISSION_ERROR_MSG =
             "User is not permitted to call service: ";
 
@@ -313,7 +328,7 @@
 
         initBackgroundInstalledPackages();
         mBackgroundInstalledPackages.add(userId, packageName);
-        mCallbackHelper.notifyAllCallbacks(userId, packageName);
+        mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_INSTALL);
         writeBackgroundInstalledPackagesToDisk();
     }
 
@@ -391,6 +406,7 @@
         initBackgroundInstalledPackages();
         mBackgroundInstalledPackages.remove(userId, packageName);
         writeBackgroundInstalledPackagesToDisk();
+        mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_UNINSTALL);
     }
 
     void handleUsageEvent(UsageEvents.Event event, int userId) {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 84a5f2b..9f4b9f1 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -64,6 +64,9 @@
 import android.util.Slog;
 import android.util.SparseArray;
 
+import com.android.internal.pm.pkg.component.ParsedActivity;
+import com.android.internal.pm.pkg.component.ParsedProvider;
+import com.android.internal.pm.pkg.component.ParsedService;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -80,6 +83,8 @@
  */
 public final class BroadcastHelper {
     private static final boolean DEBUG_BROADCASTS = false;
+    private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED =
+            "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED";
 
     private final UserManagerInternal mUmInternal;
     private final ActivityManagerInternal mAmInternal;
@@ -291,6 +296,57 @@
         return bOptions;
     }
 
+    private ArrayList<String> getAllNotExportedComponents(@NonNull AndroidPackage pkg,
+            @NonNull ArrayList<String> inputComponentNames) {
+        final ArrayList<String> outputNotExportedComponentNames = new ArrayList<>();
+        int remainingComponentCount = inputComponentNames.size();
+        for (ParsedActivity component : pkg.getReceivers()) {
+            if (inputComponentNames.contains(component.getClassName())) {
+                if (!component.isExported()) {
+                    outputNotExportedComponentNames.add(component.getClassName());
+                }
+                remainingComponentCount--;
+                if (remainingComponentCount <= 0) {
+                    return outputNotExportedComponentNames;
+                }
+            }
+        }
+        for (ParsedProvider component : pkg.getProviders()) {
+            if (inputComponentNames.contains(component.getClassName())) {
+                if (!component.isExported()) {
+                    outputNotExportedComponentNames.add(component.getClassName());
+                }
+                remainingComponentCount--;
+                if (remainingComponentCount <= 0) {
+                    return outputNotExportedComponentNames;
+                }
+            }
+        }
+        for (ParsedService component : pkg.getServices()) {
+            if (inputComponentNames.contains(component.getClassName())) {
+                if (!component.isExported()) {
+                    outputNotExportedComponentNames.add(component.getClassName());
+                }
+                remainingComponentCount--;
+                if (remainingComponentCount <= 0) {
+                    return outputNotExportedComponentNames;
+                }
+            }
+        }
+        for (ParsedActivity component : pkg.getActivities()) {
+            if (inputComponentNames.contains(component.getClassName())) {
+                if (!component.isExported()) {
+                    outputNotExportedComponentNames.add(component.getClassName());
+                }
+                remainingComponentCount--;
+                if (remainingComponentCount <= 0) {
+                    return outputNotExportedComponentNames;
+                }
+            }
+        }
+        return outputNotExportedComponentNames;
+    }
+
     private void sendPackageChangedBroadcastInternal(@NonNull String packageName,
             boolean dontKillApp,
             @NonNull ArrayList<String> componentNames,
@@ -298,10 +354,48 @@
             @Nullable String reason,
             @Nullable int[] userIds,
             @Nullable int[] instantUserIds,
-            @Nullable SparseArray<int[]> broadcastAllowList) {
-        sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames,
-                packageUid, reason, userIds, instantUserIds, broadcastAllowList,
-                null /* targetPackageName */, null /* requiredPermissions */);
+            @Nullable SparseArray<int[]> broadcastAllowList,
+            @NonNull AndroidPackage pkg) {
+        final boolean isForWholeApp = componentNames.contains(packageName);
+        if (isForWholeApp || !android.content.pm.Flags.reduceBroadcastsForComponentStateChanges()) {
+            sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames,
+                    packageUid, reason, userIds, instantUserIds, broadcastAllowList,
+                    null /* targetPackageName */, null /* requiredPermissions */);
+            return;
+        }
+        // Currently only these four components of activity, receiver, provider and service are
+        // considered to send only the broadcast to the system and the application itself when the
+        // component is not exported. In order to avoid losing to send the broadcast for other
+        // components, it gets the not exported components for these four components of activity,
+        // receiver, provider and service and the others are considered the exported components.
+        final ArrayList<String> notExportedComponentNames = getAllNotExportedComponents(pkg,
+                componentNames);
+        final ArrayList<String> exportedComponentNames = (ArrayList<String>) componentNames.clone();
+        exportedComponentNames.removeAll(notExportedComponentNames);
+
+        if (!notExportedComponentNames.isEmpty()) {
+            // Limit sending of the PACKAGE_CHANGED broadcast to only the system and the
+            // application itself when the component is not exported.
+
+            // First, send the PACKAGE_CHANGED broadcast to the system.
+            sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp,
+                    notExportedComponentNames, packageUid, reason, userIds, instantUserIds,
+                    broadcastAllowList, "android" /* targetPackageName */,
+                    new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED});
+
+            // Second, send the PACKAGE_CHANGED broadcast to the application itself.
+            sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp,
+                    notExportedComponentNames, packageUid, reason, userIds, instantUserIds,
+                    broadcastAllowList, packageName /* targetPackageName */,
+                    null /* requiredPermissions */);
+        }
+
+        if (!exportedComponentNames.isEmpty()) {
+            sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp,
+                    exportedComponentNames, packageUid, reason, userIds, instantUserIds,
+                    broadcastAllowList, null /* targetPackageName */,
+                    null /* requiredPermissions */);
+        }
     }
 
     private void sendPackageChangedBroadcastWithPermissions(@NonNull String packageName,
@@ -830,7 +924,7 @@
                                      @NonNull String reason) {
         PackageStateInternal setting = snapshot.getPackageStateInternal(packageName,
                 Process.SYSTEM_UID);
-        if (setting == null) {
+        if (setting == null || setting.getPkg() == null) {
             return;
         }
         final int userId = UserHandle.getUserId(packageUid);
@@ -842,7 +936,7 @@
                 isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds);
         mHandler.post(() -> sendPackageChangedBroadcastInternal(
                 packageName, dontKillApp, componentNames, packageUid, reason, userIds,
-                instantUserIds, broadcastAllowList));
+                instantUserIds, broadcastAllowList, setting.getPkg()));
         mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames,
                 packageUid, reason, userIds, instantUserIds, broadcastAllowList, mHandler);
     }
diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
index 2db454a..db65bf0 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
@@ -33,6 +33,7 @@
 import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.parsing.pkg.PackageImpl;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
+import com.android.internal.pm.pkg.component.AconfigFlags;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.server.pm.ApexManager;
 
@@ -41,6 +42,8 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class PackageCacher implements IPackageCacher {
@@ -57,6 +60,8 @@
     @Nullable
     private final PackageParser2.Callback mCallback;
 
+    private static final AconfigFlags sAconfigFlags = ParsingPackageUtils.getAconfigFlags();
+
     public PackageCacher(File cacheDir) {
         this(cacheDir, null);
     }
@@ -136,7 +141,7 @@
      * Given a {@code packageFile} and a {@code cacheFile} returns whether the
      * cache file is up to date based on the mod-time of both files.
      */
-    private static boolean isCacheUpToDate(File packageFile, File cacheFile) {
+    private static boolean isCacheFileUpToDate(File packageFile, File cacheFile) {
         try {
             // In case packageFile is located on one of /apex mount points it's mtime will always be
             // 0. Instead, we can use mtime of the APEX file backing the corresponding mount point.
@@ -185,16 +190,36 @@
 
         try {
             // If the cache is not up to date, return null.
-            if (!isCacheUpToDate(packageFile, cacheFile)) {
+            if (!isCacheFileUpToDate(packageFile, cacheFile)) {
                 return null;
             }
 
             final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath());
-            ParsedPackage parsed = fromCacheEntry(bytes);
+            final ParsedPackage parsed = fromCacheEntry(bytes);
             if (!packageFile.getAbsolutePath().equals(parsed.getPath())) {
                 // Don't use this cache if the path doesn't match
                 return null;
             }
+
+            if (!android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) {
+                return parsed;
+            }
+
+            final Map<String, Boolean> featureFlagState =
+                    ((PackageImpl) parsed).getFeatureFlagState();
+            if (!featureFlagState.isEmpty()) {
+                Slog.d(TAG, "Feature flags for package " + packageFile + ": " + featureFlagState);
+                for (var entry : featureFlagState.entrySet()) {
+                    final String flagPackageAndName = entry.getKey();
+                    if (!Objects.equals(sAconfigFlags.getFlagValue(flagPackageAndName),
+                            entry.getValue())) {
+                        Slog.i(TAG, "Feature flag " + flagPackageAndName + " changed for package "
+                                + packageFile + "; cached result is invalid");
+                        return null;
+                    }
+                }
+            }
+
             return parsed;
         } catch (Throwable e) {
             Slog.w(TAG, "Error reading package cache: ", e);
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index bc6a40a..07fd1cb 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1271,6 +1271,7 @@
      */
     private boolean isFixedOrUserSet(int flags) {
         return (flags & (PackageManager.FLAG_PERMISSION_USER_SET
+                | PackageManager.FLAG_PERMISSION_ONE_TIME
                 | PackageManager.FLAG_PERMISSION_USER_FIXED
                 | PackageManager.FLAG_PERMISSION_POLICY_FIXED
                 | PackageManager.FLAG_PERMISSION_SYSTEM_FIXED)) != 0;
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
index 0962319..38bc026 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
@@ -115,11 +115,18 @@
      */
     private int mPriority;
 
+    /**
+     * If resource holder retains ownership of the resource in a challenge scenario then value is
+     * true.
+     */
+    private boolean mResourceHolderRetain;
+
     private ClientProfile(Builder builder) {
         this.mId = builder.mId;
         this.mTvInputSessionId = builder.mTvInputSessionId;
         this.mUseCase = builder.mUseCase;
         this.mProcessId = builder.mProcessId;
+        this.mResourceHolderRetain = builder.mResourceHolderRetain;
     }
 
     public int getId() {
@@ -139,6 +146,14 @@
     }
 
     /**
+     * Returns true when the resource holder retains ownership of the resource in a challenge
+     * scenario.
+     */
+    public boolean shouldResourceHolderRetain() {
+        return mResourceHolderRetain;
+    }
+
+    /**
      * If the client priority is overwrttien.
      */
     public boolean isPriorityOverwritten() {
@@ -180,6 +195,19 @@
     }
 
     /**
+     * Determines whether the resource holder retains ownership of the resource during a challenge
+     * scenario, when both resource holder and resource challenger have same processId and same
+     * priority.
+     *
+     * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or
+     *     false (or resourceHolderRetain not set at all) to allow the resource challenger to
+     *     acquire the resource. If not explicitly set, resourceHolderRetain is set to false.
+     */
+    public void setResourceHolderRetain(boolean resourceHolderRetain) {
+        mResourceHolderRetain = resourceHolderRetain;
+    }
+
+    /**
      * Set when the client starts to use a frontend.
      *
      * @param frontendHandle being used.
@@ -361,6 +389,7 @@
         private String mTvInputSessionId;
         private int mUseCase;
         private int mProcessId;
+        private boolean mResourceHolderRetain = false;
 
         Builder(int id) {
             this.mId = id;
@@ -397,6 +426,18 @@
         }
 
         /**
+         * Builder for {@link ClientProfile}.
+         *
+         * @param resourceHolderRetain the determining factor for resource ownership during
+         *     challenger scenario. The default behavior favors the resource challenger and grants
+         *     them ownership of the resource if resourceHolderRetain is not explicitly set to true.
+         */
+        public Builder resourceHolderRetain(boolean resourceHolderRetain) {
+            this.mResourceHolderRetain = resourceHolderRetain;
+            return this;
+        }
+
+        /**
           * Build a {@link ClientProfile}.
           *
           * @return {@link ClientProfile}.
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index c5b6bbf..5ae8c11 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.tv.tunerresourcemanager;
 
+import static android.media.tv.flags.Flags.setResourceHolderRetain;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -229,6 +231,14 @@
         }
 
         @Override
+        public void setResourceHolderRetain(int clientId, boolean resourceHolderRetain) {
+            enforceTrmAccessPermission("setResourceHolderRetain");
+            synchronized (mLock) {
+                getClientProfile(clientId).setResourceHolderRetain(resourceHolderRetain);
+            }
+        }
+
+        @Override
         public boolean isLowestPriority(int clientId, int frontendType)
                 throws RemoteException {
             enforceTrmAccessPermission("isLowestPriority");
@@ -1066,8 +1076,10 @@
             // request client has higher priority.
             if (inUseLowestPriorityFrontend != null
                     && ((requestClient.getPriority() > currentLowestPriority)
-                    || ((requestClient.getPriority() == currentLowestPriority)
-                    && isRequestFromSameProcess))) {
+                            || ((requestClient.getPriority() == currentLowestPriority)
+                                    && isRequestFromSameProcess
+                                    && !(setResourceHolderRetain()
+                                            && requestClient.shouldResourceHolderRetain())))) {
                 frontendHandle[0] = inUseLowestPriorityFrontend.getHandle();
                 reclaimOwnerId[0] = inUseLowestPriorityFrontend.getOwnerClientId();
                 return true;
@@ -1249,9 +1261,11 @@
             // When all the resources are occupied, grant the lowest priority resource if the
             // request client has higher priority.
             if (inUseLowestPriorityLnb != null
-                    && ((requestClient.getPriority() > currentLowestPriority) || (
-                    (requestClient.getPriority() == currentLowestPriority)
-                        && isRequestFromSameProcess))) {
+                    && ((requestClient.getPriority() > currentLowestPriority)
+                            || ((requestClient.getPriority() == currentLowestPriority)
+                                    && isRequestFromSameProcess
+                                    && !(setResourceHolderRetain()
+                                            && requestClient.shouldResourceHolderRetain())))) {
                 lnbHandle[0] = inUseLowestPriorityLnb.getHandle();
                 reclaimOwnerId[0] = inUseLowestPriorityLnb.getOwnerClientId();
                 return true;
@@ -1335,8 +1349,10 @@
             // request client has higher priority.
             if (lowestPriorityOwnerId != INVALID_CLIENT_ID
                     && ((requestClient.getPriority() > currentLowestPriority)
-                    || ((requestClient.getPriority() == currentLowestPriority)
-                    && isRequestFromSameProcess))) {
+                            || ((requestClient.getPriority() == currentLowestPriority)
+                                    && isRequestFromSameProcess
+                                    && !(setResourceHolderRetain()
+                                            && requestClient.shouldResourceHolderRetain())))) {
                 casSessionHandle[0] = cas.getHandle();
                 reclaimOwnerId[0] = lowestPriorityOwnerId;
                 return true;
@@ -1420,8 +1436,10 @@
             // request client has higher priority.
             if (lowestPriorityOwnerId != INVALID_CLIENT_ID
                     && ((requestClient.getPriority() > currentLowestPriority)
-                    || ((requestClient.getPriority() == currentLowestPriority)
-                    && isRequestFromSameProcess))) {
+                            || ((requestClient.getPriority() == currentLowestPriority)
+                                    && isRequestFromSameProcess
+                                    && !(setResourceHolderRetain()
+                                            && requestClient.shouldResourceHolderRetain())))) {
                 ciCamHandle[0] = ciCam.getHandle();
                 reclaimOwnerId[0] = lowestPriorityOwnerId;
                 return true;
@@ -1655,9 +1673,11 @@
             // When all the resources are occupied, grant the lowest priority resource if the
             // request client has higher priority.
             if (inUseLowestPriorityDemux != null
-                    && ((requestClient.getPriority() > currentLowestPriority) || (
-                    (requestClient.getPriority() == currentLowestPriority)
-                        && isRequestFromSameProcess))) {
+                    && ((requestClient.getPriority() > currentLowestPriority)
+                            || ((requestClient.getPriority() == currentLowestPriority)
+                                    && isRequestFromSameProcess
+                                    && !(setResourceHolderRetain()
+                                            && requestClient.shouldResourceHolderRetain())))) {
                 demuxHandle[0] = inUseLowestPriorityDemux.getHandle();
                 reclaimOwnerId[0] = inUseLowestPriorityDemux.getOwnerClientId();
                 return true;
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
index baf84cf..3392d03 100644
--- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -436,6 +436,17 @@
             return mPrivilegedPackages.keySet();
         }
 
+        /** Returns all subscription groups */
+        @NonNull
+        public Set<ParcelUuid> getAllSubscriptionGroups() {
+            final Set<ParcelUuid> subGroups = new ArraySet<>();
+            for (SubscriptionInfo subInfo : mSubIdToInfoMap.values()) {
+                subGroups.add(subInfo.getGroupUuid());
+            }
+
+            return subGroups;
+        }
+
         /** Checks if the provided package is carrier privileged for the specified sub group. */
         public boolean packageHasPermissionsForSubscriptionGroup(
                 @NonNull ParcelUuid subGrp, @NonNull String packageName) {
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index ab5316f..92ce251 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.server.webkit;
 
-import static android.webkit.Flags.updateServiceV2;
-
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -253,46 +251,11 @@
     }
 
     @Override
-    public int getMultiProcessSetting() {
-        if (updateServiceV2()) {
-            throw new IllegalStateException(
-                    "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
-        }
-        return Settings.Global.getInt(
-                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
-    }
-
-    @Override
-    public void setMultiProcessSetting(int value) {
-        if (updateServiceV2()) {
-            throw new IllegalStateException(
-                    "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
-        }
-        Settings.Global.putInt(
-                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
-    }
-
-    @Override
-    public void notifyZygote(boolean enableMultiProcess) {
-        if (updateServiceV2()) {
-            throw new IllegalStateException(
-                    "notifyZygote shouldn't be called if update_service_v2 flag is set.");
-        }
-        WebViewZygote.setMultiprocessEnabled(enableMultiProcess);
-    }
-
-    @Override
     public void ensureZygoteStarted() {
         WebViewZygote.getProcess();
     }
 
     @Override
-    public boolean isMultiProcessDefaultEnabled() {
-        // Multiprocess is enabled by default for all devices.
-        return true;
-    }
-
-    @Override
     public void pinWebviewIfRequired(ApplicationInfo appInfo) {
         PinnerService pinnerService = LocalServices.getService(PinnerService.class);
         int webviewPinQuota = pinnerService.getWebviewPinQuota();
diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java
index 3b77d07..6710554 100644
--- a/services/core/java/com/android/server/webkit/SystemInterface.java
+++ b/services/core/java/com/android/server/webkit/SystemInterface.java
@@ -55,12 +55,8 @@
      */
     List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo);
 
-    int getMultiProcessSetting();
-    void setMultiProcessSetting(int value);
-    void notifyZygote(boolean enableMultiProcess);
     /** Start the zygote if it's not already running. */
     void ensureZygoteStarted();
-    boolean isMultiProcessDefaultEnabled();
 
     void pinWebviewIfRequired(ApplicationInfo appInfo);
 }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index 7acb864..744c3da6 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -16,8 +16,6 @@
 
 package com.android.server.webkit;
 
-import static android.webkit.Flags.updateServiceV2;
-
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -63,7 +61,7 @@
             new Histogram.ScaledRangeOptions(20, 0, 1, 1.4f));
 
     private BroadcastReceiver mWebViewUpdatedReceiver;
-    private WebViewUpdateServiceInterface mImpl;
+    private WebViewUpdateServiceImpl2 mImpl;
 
     static final int PACKAGE_CHANGED = 0;
     static final int PACKAGE_ADDED = 1;
@@ -72,11 +70,7 @@
 
     public WebViewUpdateService(Context context) {
         super(context);
-        if (updateServiceV2()) {
-            mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context));
-        } else {
-            mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context));
-        }
+        mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context));
     }
 
     @Override
@@ -170,13 +164,8 @@
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
-            if (updateServiceV2()) {
-                (new WebViewUpdateServiceShellCommand2(this))
-                        .exec(this, in, out, err, args, callback, resultReceiver);
-            } else {
-                (new WebViewUpdateServiceShellCommand(this))
-                        .exec(this, in, out, err, args, callback, resultReceiver);
-            }
+            (new WebViewUpdateServiceShellCommand2(this))
+                    .exec(this, in, out, err, args, callback, resultReceiver);
         }
 
 
@@ -300,45 +289,6 @@
             return currentWebViewPackage;
         }
 
-        @Override // Binder call
-        public boolean isMultiProcessEnabled() {
-            if (updateServiceV2()) {
-                throw new IllegalStateException(
-                        "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is"
-                                + " set.");
-            }
-            return WebViewUpdateService.this.mImpl.isMultiProcessEnabled();
-        }
-
-        @Override // Binder call
-        public void enableMultiProcess(boolean enable) {
-            if (updateServiceV2()) {
-                throw new IllegalStateException(
-                        "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
-            }
-            if (getContext()
-                            .checkCallingPermission(
-                                    android.Manifest.permission.WRITE_SECURE_SETTINGS)
-                    != PackageManager.PERMISSION_GRANTED) {
-                String msg =
-                        "Permission Denial: enableMultiProcess() from pid="
-                                + Binder.getCallingPid()
-                                + ", uid="
-                                + Binder.getCallingUid()
-                                + " requires "
-                                + android.Manifest.permission.WRITE_SECURE_SETTINGS;
-                Slog.w(TAG, msg);
-                throw new SecurityException(msg);
-            }
-
-            final long callingId = Binder.clearCallingIdentity();
-            try {
-                WebViewUpdateService.this.mImpl.enableMultiProcess(enable);
-            } finally {
-                Binder.restoreCallingIdentity(callingId);
-            }
-        }
-
         @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
deleted file mode 100644
index b9be4a2..0000000
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ /dev/null
@@ -1,768 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.webkit;
-
-import android.annotation.Nullable;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.Signature;
-import android.os.AsyncTask;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.util.AndroidRuntimeException;
-import android.util.Slog;
-import android.webkit.UserPackage;
-import android.webkit.WebViewFactory;
-import android.webkit.WebViewProviderInfo;
-import android.webkit.WebViewProviderResponse;
-
-import com.android.modules.expresslog.Counter;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Implementation of the WebViewUpdateService.
- * This class doesn't depend on the android system like the actual Service does and can be used
- * directly by tests (as long as they implement a SystemInterface).
- *
- * This class keeps track of and prepares the current WebView implementation, and needs to keep
- * track of a couple of different things such as what package is used as WebView implementation.
- *
- * The package-visible methods in this class are accessed from WebViewUpdateService either on the UI
- * thread or on one of multiple Binder threads. The WebView preparation code shares state between
- * threads meaning that code that chooses a new WebView implementation or checks which
- * implementation is being used needs to hold a lock.
- *
- * The WebViewUpdateService can be accessed in a couple of different ways.
- * 1. It is started from the SystemServer at boot - at that point we just initiate some state such
- * as the WebView preparation class.
- * 2. The SystemServer calls WebViewUpdateService.prepareWebViewInSystemServer. This happens at boot
- * and the WebViewUpdateService should not have been accessed before this call. In this call we
- * choose WebView implementation for the first time.
- * 3. The update service listens for Intents related to package installs and removals. These intents
- * are received and processed on the UI thread. Each intent can result in changing WebView
- * implementation.
- * 4. The update service can be reached through Binder calls which are handled on specific binder
- * threads. These calls can be made from any process. Generally they are used for changing WebView
- * implementation (from Settings), getting information about the current WebView implementation (for
- * loading WebView into an app process), or notifying the service about Relro creation being
- * completed.
- *
- * @hide
- */
-class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface {
-    private static final String TAG = WebViewUpdateServiceImpl.class.getSimpleName();
-
-    private static class WebViewPackageMissingException extends Exception {
-        WebViewPackageMissingException(String message) {
-            super(message);
-        }
-
-        WebViewPackageMissingException(Exception e) {
-            super(e);
-        }
-    }
-
-    private static final int WAIT_TIMEOUT_MS = 1000; // KEY_DISPATCHING_TIMEOUT is 5000.
-    private static final long NS_PER_MS = 1000000;
-
-    private static final int VALIDITY_OK = 0;
-    private static final int VALIDITY_INCORRECT_SDK_VERSION = 1;
-    private static final int VALIDITY_INCORRECT_VERSION_CODE = 2;
-    private static final int VALIDITY_INCORRECT_SIGNATURE = 3;
-    private static final int VALIDITY_NO_LIBRARY_FLAG = 4;
-
-    private static final int MULTIPROCESS_SETTING_ON_VALUE = Integer.MAX_VALUE;
-    private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE;
-
-    private final SystemInterface mSystemInterface;
-
-    private long mMinimumVersionCode = -1;
-
-    // Keeps track of the number of running relro creations
-    private int mNumRelroCreationsStarted = 0;
-    private int mNumRelroCreationsFinished = 0;
-    // Implies that we need to rerun relro creation because we are using an out-of-date package
-    private boolean mWebViewPackageDirty = false;
-    private boolean mAnyWebViewInstalled = false;
-
-    private static final int NUMBER_OF_RELROS_UNKNOWN = Integer.MAX_VALUE;
-
-    // The WebView package currently in use (or the one we are preparing).
-    private PackageInfo mCurrentWebViewPackage = null;
-
-    private final Object mLock = new Object();
-
-    WebViewUpdateServiceImpl(SystemInterface systemInterface) {
-        mSystemInterface = systemInterface;
-    }
-
-    @Override
-    public void packageStateChanged(String packageName, int changedState, int userId) {
-        // We don't early out here in different cases where we could potentially early-out (e.g. if
-        // we receive PACKAGE_CHANGED for another user than the system user) since that would
-        // complicate this logic further and open up for more edge cases.
-        for (WebViewProviderInfo provider : mSystemInterface.getWebViewPackages()) {
-            String webviewPackage = provider.packageName;
-
-            if (webviewPackage.equals(packageName)) {
-                boolean updateWebView = false;
-                boolean removedOrChangedOldPackage = false;
-                String oldProviderName = null;
-                PackageInfo newPackage = null;
-                synchronized (mLock) {
-                    try {
-                        newPackage = findPreferredWebViewPackage();
-                        if (mCurrentWebViewPackage != null) {
-                            oldProviderName = mCurrentWebViewPackage.packageName;
-                        }
-                        // Only trigger update actions if the updated package is the one
-                        // that will be used, or the one that was in use before the
-                        // update, or if we haven't seen a valid WebView package before.
-                        updateWebView =
-                            provider.packageName.equals(newPackage.packageName)
-                            || provider.packageName.equals(oldProviderName)
-                            || mCurrentWebViewPackage == null;
-                        // We removed the old package if we received an intent to remove
-                        // or replace the old package.
-                        removedOrChangedOldPackage =
-                            provider.packageName.equals(oldProviderName);
-                        if (updateWebView) {
-                            onWebViewProviderChanged(newPackage);
-                        }
-                    } catch (WebViewPackageMissingException e) {
-                        mCurrentWebViewPackage = null;
-                        Slog.e(TAG, "Could not find valid WebView package to create relro with "
-                                + e);
-                    }
-                }
-                if (updateWebView && !removedOrChangedOldPackage
-                        && oldProviderName != null) {
-                    // If the provider change is the result of adding or replacing a
-                    // package that was not the previous provider then we must kill
-                    // packages dependent on the old package ourselves. The framework
-                    // only kills dependents of packages that are being removed.
-                    mSystemInterface.killPackageDependents(oldProviderName);
-                }
-                return;
-            }
-        }
-    }
-
-    @Override
-    public void prepareWebViewInSystemServer() {
-        mSystemInterface.notifyZygote(isMultiProcessEnabled());
-        try {
-            synchronized (mLock) {
-                mCurrentWebViewPackage = findPreferredWebViewPackage();
-                String userSetting = mSystemInterface.getUserChosenWebViewProvider();
-                if (userSetting != null
-                        && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
-                    // Don't persist the user-chosen setting across boots if the package being
-                    // chosen is not used (could be disabled or uninstalled) so that the user won't
-                    // be surprised by the device switching to using a certain webview package,
-                    // that was uninstalled/disabled a long time ago, if it is installed/enabled
-                    // again.
-                    mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName);
-                }
-                onWebViewProviderChanged(mCurrentWebViewPackage);
-            }
-        } catch (WebViewPackageMissingException e) {
-            Slog.e(TAG, "Could not find valid WebView package to create relro with", e);
-        } catch (Throwable t) {
-            // We don't know a case when this should happen but we log and discard errors at this
-            // stage as we must not crash the system server.
-            Slog.wtf(TAG, "error preparing webview provider from system server", t);
-        }
-
-        if (getCurrentWebViewPackage() == null) {
-            // We didn't find a valid WebView implementation. Try explicitly re-enabling the
-            // fallback package for all users in case it was disabled, even if we already did the
-            // one-time migration before. If this actually changes the state, we will see the
-            // PackageManager broadcast shortly and try again.
-            WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
-            WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
-            if (fallbackProvider != null) {
-                Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
-                mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true);
-            } else {
-                Slog.e(TAG, "No valid provider and no fallback available.");
-            }
-        }
-    }
-
-    private void startZygoteWhenReady() {
-        // Wait on a background thread for RELRO creation to be done. We ignore the return value
-        // because even if RELRO creation failed we still want to start the zygote.
-        waitForAndGetProvider();
-        mSystemInterface.ensureZygoteStarted();
-    }
-
-    @Override
-    public void handleNewUser(int userId) {
-        // The system user is always started at boot, and by that point we have already run one
-        // round of the package-changing logic (through prepareWebViewInSystemServer()), so early
-        // out here.
-        if (userId == UserHandle.USER_SYSTEM) return;
-        handleUserChange();
-    }
-
-    @Override
-    public void handleUserRemoved(int userId) {
-        handleUserChange();
-    }
-
-    /**
-     * Called when a user was added or removed to ensure WebView preparation is triggered.
-     * This has to be done since the WebView package we use depends on the enabled-state
-     * of packages for all users (so adding or removing a user might cause us to change package).
-     */
-    private void handleUserChange() {
-        // Potentially trigger package-changing logic.
-        updateCurrentWebViewPackage(null);
-    }
-
-    @Override
-    public void notifyRelroCreationCompleted() {
-        synchronized (mLock) {
-            mNumRelroCreationsFinished++;
-            checkIfRelrosDoneLocked();
-        }
-    }
-
-    @Override
-    public WebViewProviderResponse waitForAndGetProvider() {
-        PackageInfo webViewPackage = null;
-        final long timeoutTimeMs = System.nanoTime() / NS_PER_MS + WAIT_TIMEOUT_MS;
-        boolean webViewReady = false;
-        int webViewStatus = WebViewFactory.LIBLOAD_SUCCESS;
-        synchronized (mLock) {
-            webViewReady = webViewIsReadyLocked();
-            while (!webViewReady) {
-                final long timeNowMs = System.nanoTime() / NS_PER_MS;
-                if (timeNowMs >= timeoutTimeMs) break;
-                try {
-                    mLock.wait(timeoutTimeMs - timeNowMs);
-                } catch (InterruptedException e) {
-                    // ignore
-                }
-                webViewReady = webViewIsReadyLocked();
-            }
-            // Make sure we return the provider that was used to create the relro file
-            webViewPackage = mCurrentWebViewPackage;
-            if (webViewReady) {
-                // success
-            } else if (!mAnyWebViewInstalled) {
-                webViewStatus = WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES;
-            } else {
-                // Either the current relro creation  isn't done yet, or the new relro creatioin
-                // hasn't kicked off yet (the last relro creation used an out-of-date WebView).
-                webViewStatus = WebViewFactory.LIBLOAD_FAILED_WAITING_FOR_RELRO;
-                String timeoutError = "Timed out waiting for relro creation, relros started "
-                        + mNumRelroCreationsStarted
-                        + " relros finished " + mNumRelroCreationsFinished
-                        + " package dirty? " + mWebViewPackageDirty;
-                Slog.e(TAG, timeoutError);
-                Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, timeoutError);
-            }
-        }
-        if (!webViewReady) Slog.w(TAG, "creating relro file timed out");
-        return new WebViewProviderResponse(webViewPackage, webViewStatus);
-    }
-
-    /**
-     * Change WebView provider and provider setting and kill packages using the old provider.
-     * Return the new provider (in case we are in the middle of creating relro files, or
-     * replacing that provider it will not be in use directly, but will be used when the relros
-     * or the replacement are done).
-     */
-    @Override
-    public String changeProviderAndSetting(String newProviderName) {
-        PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName);
-        if (newPackage == null) return "";
-        return newPackage.packageName;
-    }
-
-    /**
-     * Update the current WebView package.
-     * @param newProviderName the package to switch to, null if no package has been explicitly
-     * chosen.
-     */
-    private PackageInfo updateCurrentWebViewPackage(@Nullable String newProviderName) {
-        PackageInfo oldPackage = null;
-        PackageInfo newPackage = null;
-        boolean providerChanged = false;
-        synchronized (mLock) {
-            oldPackage = mCurrentWebViewPackage;
-
-            if (newProviderName != null) {
-                mSystemInterface.updateUserSetting(newProviderName);
-            }
-
-            try {
-                newPackage = findPreferredWebViewPackage();
-                providerChanged = (oldPackage == null)
-                        || !newPackage.packageName.equals(oldPackage.packageName);
-            } catch (WebViewPackageMissingException e) {
-                // If updated the Setting but don't have an installed WebView package, the
-                // Setting will be used when a package is available.
-                mCurrentWebViewPackage = null;
-                Slog.e(TAG, "Couldn't find WebView package to use " + e);
-                return null;
-            }
-            // Perform the provider change if we chose a new provider
-            if (providerChanged) {
-                onWebViewProviderChanged(newPackage);
-            }
-        }
-        // Kill apps using the old provider only if we changed provider
-        if (providerChanged && oldPackage != null) {
-            mSystemInterface.killPackageDependents(oldPackage.packageName);
-        }
-        // Return the new provider, this is not necessarily the one we were asked to switch to,
-        // but the persistent setting will now be pointing to the provider we were asked to
-        // switch to anyway.
-        return newPackage;
-    }
-
-    /**
-     * This is called when we change WebView provider, either when the current provider is
-     * updated or a new provider is chosen / takes precedence.
-     */
-    private void onWebViewProviderChanged(PackageInfo newPackage) {
-        synchronized (mLock) {
-            mAnyWebViewInstalled = true;
-            if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) {
-                mSystemInterface.pinWebviewIfRequired(newPackage.applicationInfo);
-                mCurrentWebViewPackage = newPackage;
-
-                // The relro creations might 'finish' (not start at all) before
-                // WebViewFactory.onWebViewProviderChanged which means we might not know the
-                // number of started creations before they finish.
-                mNumRelroCreationsStarted = NUMBER_OF_RELROS_UNKNOWN;
-                mNumRelroCreationsFinished = 0;
-                mNumRelroCreationsStarted =
-                    mSystemInterface.onWebViewProviderChanged(newPackage);
-                Counter.logIncrement("webview.value_on_webview_provider_changed_counter");
-                if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) {
-                    Counter.logIncrement(
-                            "webview.value_on_webview_provider_changed_"
-                            + "with_default_package_counter");
-                }
-                // If the relro creations finish before we know the number of started creations
-                // we will have to do any cleanup/notifying here.
-                checkIfRelrosDoneLocked();
-            } else {
-                mWebViewPackageDirty = true;
-            }
-        }
-
-        // Once we've notified the system that the provider has changed and started RELRO creation,
-        // try to restart the zygote so that it will be ready when apps use it.
-        if (isMultiProcessEnabled()) {
-            AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady);
-        }
-    }
-
-    /**
-     * Fetch only the currently valid WebView packages.
-     **/
-    @Override
-    public WebViewProviderInfo[] getValidWebViewPackages() {
-        ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos();
-        WebViewProviderInfo[] providers =
-            new WebViewProviderInfo[providersAndPackageInfos.length];
-        for (int n = 0; n < providersAndPackageInfos.length; n++) {
-            providers[n] = providersAndPackageInfos[n].provider;
-        }
-        return providers;
-    }
-
-    @Override
-    public WebViewProviderInfo getDefaultWebViewPackage() {
-        for (WebViewProviderInfo provider : getWebViewPackages()) {
-            if (provider.availableByDefault) {
-                return provider;
-            }
-        }
-
-        // This should be unreachable because the config parser enforces that there is at least
-        // one availableByDefault provider.
-        throw new AndroidRuntimeException("No available by default WebView Provider.");
-    }
-
-    private static class ProviderAndPackageInfo {
-        public final WebViewProviderInfo provider;
-        public final PackageInfo packageInfo;
-
-        ProviderAndPackageInfo(WebViewProviderInfo provider, PackageInfo packageInfo) {
-            this.provider = provider;
-            this.packageInfo = packageInfo;
-        }
-    }
-
-    private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos() {
-        WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages();
-        List<ProviderAndPackageInfo> providers = new ArrayList<>();
-        for (int n = 0; n < allProviders.length; n++) {
-            try {
-                PackageInfo packageInfo =
-                        mSystemInterface.getPackageInfoForProvider(allProviders[n]);
-                if (validityResult(allProviders[n], packageInfo) == VALIDITY_OK) {
-                    providers.add(new ProviderAndPackageInfo(allProviders[n], packageInfo));
-                }
-            } catch (NameNotFoundException e) {
-                // Don't add non-existent packages
-            }
-        }
-        return providers.toArray(new ProviderAndPackageInfo[providers.size()]);
-    }
-
-    /**
-     * Returns either the package info of the WebView provider determined in the following way:
-     * If the user has chosen a provider then use that if it is valid,
-     * otherwise use the first package in the webview priority list that is valid.
-     *
-     */
-    private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
-        ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
-
-        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider();
-
-        // If the user has chosen provider, use that (if it's installed and enabled for all
-        // users).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
-                }
-            }
-        }
-
-        // User did not choose, or the choice failed; use the most stable provider that is
-        // installed and enabled for all users, and available by default (not through
-        // user choice).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.availableByDefault) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
-                }
-            }
-        }
-
-        // This should never happen during normal operation (only with modified system images).
-        mAnyWebViewInstalled = false;
-        throw new WebViewPackageMissingException("Could not find a loadable WebView package");
-    }
-
-    /**
-     * Return true iff {@param packageInfos} point to only installed and enabled packages.
-     * The given packages {@param packageInfos} should all be pointing to the same package, but each
-     * PackageInfo representing a different user's package.
-     */
-    private static boolean isInstalledAndEnabledForAllUsers(
-            List<UserPackage> userPackages) {
-        for (UserPackage userPackage : userPackages) {
-            if (!userPackage.isInstalledPackage() || !userPackage.isEnabledPackage()) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public WebViewProviderInfo[] getWebViewPackages() {
-        return mSystemInterface.getWebViewPackages();
-    }
-
-    @Override
-    public PackageInfo getCurrentWebViewPackage() {
-        synchronized (mLock) {
-            return mCurrentWebViewPackage;
-        }
-    }
-
-    /**
-     * Returns whether WebView is ready and is not going to go through its preparation phase
-     * again directly.
-     */
-    private boolean webViewIsReadyLocked() {
-        return !mWebViewPackageDirty
-            && (mNumRelroCreationsStarted == mNumRelroCreationsFinished)
-            // The current package might be replaced though we haven't received an intent
-            // declaring this yet, the following flag makes anyone loading WebView to wait in
-            // this case.
-            && mAnyWebViewInstalled;
-    }
-
-    private void checkIfRelrosDoneLocked() {
-        if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) {
-            if (mWebViewPackageDirty) {
-                mWebViewPackageDirty = false;
-                // If we have changed provider since we started the relro creation we need to
-                // redo the whole process using the new package instead.
-                try {
-                    PackageInfo newPackage = findPreferredWebViewPackage();
-                    onWebViewProviderChanged(newPackage);
-                } catch (WebViewPackageMissingException e) {
-                    mCurrentWebViewPackage = null;
-                    // If we can't find any valid WebView package we are now in a state where
-                    // mAnyWebViewInstalled is false, so loading WebView will be blocked and we
-                    // should simply wait until we receive an intent declaring a new package was
-                    // installed.
-                }
-            } else {
-                mLock.notifyAll();
-            }
-        }
-    }
-
-    private int validityResult(WebViewProviderInfo configInfo, PackageInfo packageInfo) {
-        // Ensure the provider targets this framework release (or a later one).
-        if (!UserPackage.hasCorrectTargetSdkVersion(packageInfo)) {
-            return VALIDITY_INCORRECT_SDK_VERSION;
-        }
-        if (!versionCodeGE(packageInfo.getLongVersionCode(), getMinimumVersionCode())
-                && !mSystemInterface.systemIsDebuggable()) {
-            // Webview providers may be downgraded arbitrarily low, prevent that by enforcing
-            // minimum version code. This check is only enforced for user builds.
-            return VALIDITY_INCORRECT_VERSION_CODE;
-        }
-        if (!providerHasValidSignature(configInfo, packageInfo, mSystemInterface)) {
-            return VALIDITY_INCORRECT_SIGNATURE;
-        }
-        if (WebViewFactory.getWebViewLibrary(packageInfo.applicationInfo) == null) {
-            return VALIDITY_NO_LIBRARY_FLAG;
-        }
-        return VALIDITY_OK;
-    }
-
-    /**
-     * Both versionCodes should be from a WebView provider package implemented by Chromium.
-     * VersionCodes from other kinds of packages won't make any sense in this method.
-     *
-     * An introduction to Chromium versionCode scheme:
-     * "BBBBPPPXX"
-     * BBBB: 4 digit branch number. It monotonically increases over time.
-     * PPP: patch number in the branch. It is padded with zeroes to the left. These three digits
-     * may change their meaning in the future.
-     * XX: Digits to differentiate different APK builds of the same source version.
-     *
-     * This method takes the "BBBB" of versionCodes and compare them.
-     *
-     * https://www.chromium.org/developers/version-numbers describes general Chromium versioning;
-     * https://source.chromium.org/chromium/chromium/src/+/master:build/util/android_chrome_version.py
-     * is the canonical source for how Chromium versionCodes are calculated.
-     *
-     * @return true if versionCode1 is higher than or equal to versionCode2.
-     */
-    private static boolean versionCodeGE(long versionCode1, long versionCode2) {
-        long v1 = versionCode1 / 100000;
-        long v2 = versionCode2 / 100000;
-
-        return v1 >= v2;
-    }
-
-    /**
-     * Gets the minimum version code allowed for a valid provider. It is the minimum versionCode
-     * of all available-by-default WebView provider packages. If there is no such WebView provider
-     * package on the system, then return -1, which means all positive versionCode WebView packages
-     * are accepted.
-     *
-     * Note that this is a private method that handles a variable (mMinimumVersionCode) which is
-     * shared between threads. Furthermore, this method does not hold mLock meaning that we must
-     * take extra care to ensure this method is thread-safe.
-     */
-    private long getMinimumVersionCode() {
-        if (mMinimumVersionCode > 0) {
-            return mMinimumVersionCode;
-        }
-
-        long minimumVersionCode = -1;
-        for (WebViewProviderInfo provider : mSystemInterface.getWebViewPackages()) {
-            if (provider.availableByDefault) {
-                try {
-                    long versionCode =
-                            mSystemInterface.getFactoryPackageVersion(provider.packageName);
-                    if (minimumVersionCode < 0 || versionCode < minimumVersionCode) {
-                        minimumVersionCode = versionCode;
-                    }
-                } catch (NameNotFoundException e) {
-                    // Safe to ignore.
-                }
-            }
-        }
-
-        mMinimumVersionCode = minimumVersionCode;
-        return mMinimumVersionCode;
-    }
-
-    private static boolean providerHasValidSignature(WebViewProviderInfo provider,
-            PackageInfo packageInfo, SystemInterface systemInterface) {
-        // Skip checking signatures on debuggable builds, for development purposes.
-        if (systemInterface.systemIsDebuggable()) return true;
-
-        // Allow system apps to be valid providers regardless of signature.
-        if (packageInfo.applicationInfo.isSystemApp()) return true;
-
-        // We don't support packages with multiple signatures.
-        if (packageInfo.signatures.length != 1) return false;
-
-        // If any of the declared signatures match the package signature, it's valid.
-        for (Signature signature : provider.signatures) {
-            if (signature.equals(packageInfo.signatures[0])) return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Returns the only fallback provider in the set of given packages, or null if there is none.
-     */
-    private static WebViewProviderInfo getFallbackProvider(WebViewProviderInfo[] webviewPackages) {
-        for (WebViewProviderInfo provider : webviewPackages) {
-            if (provider.isFallback) {
-                return provider;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public boolean isMultiProcessEnabled() {
-        int settingValue = mSystemInterface.getMultiProcessSetting();
-        if (mSystemInterface.isMultiProcessDefaultEnabled()) {
-            // Multiprocess should be enabled unless the user has turned it off manually.
-            return settingValue > MULTIPROCESS_SETTING_OFF_VALUE;
-        } else {
-            // Multiprocess should not be enabled, unless the user has turned it on manually.
-            return settingValue >= MULTIPROCESS_SETTING_ON_VALUE;
-        }
-    }
-
-    @Override
-    public void enableMultiProcess(boolean enable) {
-        PackageInfo current = getCurrentWebViewPackage();
-        mSystemInterface.setMultiProcessSetting(
-                enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE);
-        mSystemInterface.notifyZygote(enable);
-        if (current != null) {
-            mSystemInterface.killPackageDependents(current.packageName);
-        }
-    }
-
-    /**
-     * Dump the state of this Service.
-     */
-    @Override
-    public void dumpState(PrintWriter pw) {
-        pw.println("Current WebView Update Service state");
-        pw.println(String.format("  Multiprocess enabled: %b", isMultiProcessEnabled()));
-        synchronized (mLock) {
-            if (mCurrentWebViewPackage == null) {
-                pw.println("  Current WebView package is null");
-            } else {
-                pw.println(String.format("  Current WebView package (name, version): (%s, %s)",
-                        mCurrentWebViewPackage.packageName,
-                        mCurrentWebViewPackage.versionName));
-            }
-            pw.println(String.format("  Minimum targetSdkVersion: %d",
-                    UserPackage.MINIMUM_SUPPORTED_SDK));
-            pw.println(String.format("  Minimum WebView version code: %d",
-                    mMinimumVersionCode));
-            pw.println(String.format("  Number of relros started: %d",
-                    mNumRelroCreationsStarted));
-            pw.println(String.format("  Number of relros finished: %d",
-                        mNumRelroCreationsFinished));
-            pw.println(String.format("  WebView package dirty: %b", mWebViewPackageDirty));
-            pw.println(String.format("  Any WebView package installed: %b",
-                    mAnyWebViewInstalled));
-
-            try {
-                PackageInfo preferredWebViewPackage = findPreferredWebViewPackage();
-                pw.println(String.format(
-                        "  Preferred WebView package (name, version): (%s, %s)",
-                        preferredWebViewPackage.packageName,
-                        preferredWebViewPackage.versionName));
-            } catch (WebViewPackageMissingException e) {
-                pw.println(String.format("  Preferred WebView package: none"));
-            }
-
-            dumpAllPackageInformationLocked(pw);
-        }
-    }
-
-    private void dumpAllPackageInformationLocked(PrintWriter pw) {
-        WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages();
-        pw.println("  WebView packages:");
-        for (WebViewProviderInfo provider : allProviders) {
-            List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(provider);
-            PackageInfo systemUserPackageInfo =
-                    userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
-            if (systemUserPackageInfo == null) {
-                pw.println(String.format("    %s is NOT installed.", provider.packageName));
-                continue;
-            }
-
-            int validity = validityResult(provider, systemUserPackageInfo);
-            String packageDetails = String.format(
-                    "versionName: %s, versionCode: %d, targetSdkVersion: %d",
-                    systemUserPackageInfo.versionName,
-                    systemUserPackageInfo.getLongVersionCode(),
-                    systemUserPackageInfo.applicationInfo.targetSdkVersion);
-            if (validity == VALIDITY_OK) {
-                boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
-                        mSystemInterface.getPackageInfoForProviderAllUsers(provider));
-                pw.println(String.format(
-                        "    Valid package %s (%s) is %s installed/enabled for all users",
-                        systemUserPackageInfo.packageName,
-                        packageDetails,
-                        installedForAllUsers ? "" : "NOT"));
-            } else {
-                pw.println(String.format("    Invalid package %s (%s), reason: %s",
-                        systemUserPackageInfo.packageName,
-                        packageDetails,
-                        getInvalidityReason(validity)));
-            }
-        }
-    }
-
-    private static String getInvalidityReason(int invalidityReason) {
-        switch (invalidityReason) {
-            case VALIDITY_INCORRECT_SDK_VERSION:
-                return "SDK version too low";
-            case VALIDITY_INCORRECT_VERSION_CODE:
-                return "Version code too low";
-            case VALIDITY_INCORRECT_SIGNATURE:
-                return "Incorrect signature";
-            case VALIDITY_NO_LIBRARY_FLAG:
-                return "No WebView-library manifest flag";
-            default:
-                return "Unexcepted validity-reason";
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 307c15b..a5a02cd 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -66,7 +66,7 @@
  *
  * @hide
  */
-class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface {
+class WebViewUpdateServiceImpl2 {
     private static final String TAG = WebViewUpdateServiceImpl2.class.getSimpleName();
 
     private static class WebViewPackageMissingException extends Exception {
@@ -125,7 +125,6 @@
         mDefaultProvider = defaultProvider;
     }
 
-    @Override
     public void packageStateChanged(String packageName, int changedState, int userId) {
         // We don't early out here in different cases where we could potentially early-out (e.g. if
         // we receive PACKAGE_CHANGED for another user than the system user) since that would
@@ -216,7 +215,6 @@
         mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true);
     }
 
-    @Override
     public void prepareWebViewInSystemServer() {
         try {
             boolean repairNeeded = true;
@@ -256,7 +254,6 @@
         mSystemInterface.ensureZygoteStarted();
     }
 
-    @Override
     public void handleNewUser(int userId) {
         // The system user is always started at boot, and by that point we have already run one
         // round of the package-changing logic (through prepareWebViewInSystemServer()), so early
@@ -265,7 +262,6 @@
         handleUserChange();
     }
 
-    @Override
     public void handleUserRemoved(int userId) {
         handleUserChange();
     }
@@ -280,7 +276,6 @@
         updateCurrentWebViewPackage(null);
     }
 
-    @Override
     public void notifyRelroCreationCompleted() {
         synchronized (mLock) {
             mNumRelroCreationsFinished++;
@@ -288,7 +283,6 @@
         }
     }
 
-    @Override
     public WebViewProviderResponse waitForAndGetProvider() {
         PackageInfo webViewPackage = null;
         final long timeoutTimeMs = System.nanoTime() / NS_PER_MS + WAIT_TIMEOUT_MS;
@@ -334,7 +328,6 @@
      * replacing that provider it will not be in use directly, but will be used when the relros
      * or the replacement are done).
      */
-    @Override
     public String changeProviderAndSetting(String newProviderName) {
         PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName);
         if (newPackage == null) return "";
@@ -430,7 +423,6 @@
     }
 
     /** Fetch only the currently valid WebView packages. */
-    @Override
     public WebViewProviderInfo[] getValidWebViewPackages() {
         ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos();
         WebViewProviderInfo[] providers =
@@ -445,7 +437,6 @@
      * Returns the default WebView provider which should be first availableByDefault option in the
      * system config.
      */
-    @Override
     public WebViewProviderInfo getDefaultWebViewPackage() {
         return mDefaultProvider;
     }
@@ -550,12 +541,10 @@
         return true;
     }
 
-    @Override
     public WebViewProviderInfo[] getWebViewPackages() {
         return mSystemInterface.getWebViewPackages();
     }
 
-    @Override
     public PackageInfo getCurrentWebViewPackage() {
         synchronized (mLock) {
             return mCurrentWebViewPackage;
@@ -708,20 +697,7 @@
         return null;
     }
 
-    @Override
-    public boolean isMultiProcessEnabled() {
-        throw new IllegalStateException(
-                "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is set.");
-    }
-
-    @Override
-    public void enableMultiProcess(boolean enable) {
-        throw new IllegalStateException(
-                "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
-    }
-
     /** Dump the state of this Service. */
-    @Override
     public void dumpState(PrintWriter pw) {
         pw.println("Current WebView Update Service state");
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java
deleted file mode 100644
index 1772ef9..0000000
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.webkit;
-
-import android.content.pm.PackageInfo;
-import android.webkit.WebViewProviderInfo;
-import android.webkit.WebViewProviderResponse;
-
-import java.io.PrintWriter;
-
-interface WebViewUpdateServiceInterface {
-    void packageStateChanged(String packageName, int changedState, int userId);
-
-    void handleNewUser(int userId);
-
-    void handleUserRemoved(int userId);
-
-    WebViewProviderInfo[] getWebViewPackages();
-
-    void prepareWebViewInSystemServer();
-
-    void notifyRelroCreationCompleted();
-
-    WebViewProviderResponse waitForAndGetProvider();
-
-    String changeProviderAndSetting(String newProviderName);
-
-    WebViewProviderInfo[] getValidWebViewPackages();
-
-    WebViewProviderInfo getDefaultWebViewPackage();
-
-    PackageInfo getCurrentWebViewPackage();
-
-    boolean isMultiProcessEnabled();
-
-    void enableMultiProcess(boolean enable);
-
-    void dumpState(PrintWriter pw);
-}
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java
deleted file mode 100644
index 7529c41..0000000
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.webkit;
-
-import android.os.RemoteException;
-import android.os.ShellCommand;
-import android.webkit.IWebViewUpdateService;
-
-import java.io.PrintWriter;
-
-class WebViewUpdateServiceShellCommand extends ShellCommand {
-    final IWebViewUpdateService mInterface;
-
-    WebViewUpdateServiceShellCommand(IWebViewUpdateService service) {
-        mInterface = service;
-    }
-
-    @Override
-    public int onCommand(String cmd) {
-        if (cmd == null) {
-            return handleDefaultCommands(cmd);
-        }
-
-        final PrintWriter pw = getOutPrintWriter();
-        try {
-            switch(cmd) {
-                case "set-webview-implementation":
-                    return setWebViewImplementation();
-                case "enable-multiprocess":
-                    return enableMultiProcess(true);
-                case "disable-multiprocess":
-                    return enableMultiProcess(false);
-                default:
-                    return handleDefaultCommands(cmd);
-            }
-        } catch (RemoteException e) {
-            pw.println("Remote exception: " + e);
-        }
-        return -1;
-    }
-
-    private int setWebViewImplementation() throws RemoteException {
-        final PrintWriter pw = getOutPrintWriter();
-        String shellChosenPackage = getNextArg();
-        if (shellChosenPackage == null) {
-            pw.println("Failed to switch, no PACKAGE provided.");
-            pw.println("");
-            helpSetWebViewImplementation();
-            return 1;
-        }
-        String newPackage = mInterface.changeProviderAndSetting(shellChosenPackage);
-        if (shellChosenPackage.equals(newPackage)) {
-            pw.println("Success");
-            return 0;
-        } else {
-            pw.println(String.format(
-                        "Failed to switch to %s, the WebView implementation is now provided by %s.",
-                        shellChosenPackage, newPackage));
-            return 1;
-        }
-    }
-
-    private int enableMultiProcess(boolean enable) throws RemoteException {
-        final PrintWriter pw = getOutPrintWriter();
-        mInterface.enableMultiProcess(enable);
-        pw.println("Success");
-        return 0;
-    }
-
-    public void helpSetWebViewImplementation() {
-        PrintWriter pw = getOutPrintWriter();
-        pw.println("  set-webview-implementation PACKAGE");
-        pw.println("    Set the WebView implementation to the specified package.");
-    }
-
-    @Override
-    public void onHelp() {
-        PrintWriter pw = getOutPrintWriter();
-        pw.println("WebView updater commands:");
-        pw.println("  help");
-        pw.println("    Print this help text.");
-        pw.println("");
-        helpSetWebViewImplementation();
-        pw.println("  enable-multiprocess");
-        pw.println("    Enable multi-process mode for WebView");
-        pw.println("  disable-multiprocess");
-        pw.println("    Disable multi-process mode for WebView");
-        pw.println();
-    }
-}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index b036cfc..0745fec 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5896,6 +5896,29 @@
     }
 
     /**
+     * Registers an app that uses the Strict Mode for detecting BAL.
+     *
+     * @param callback the callback to register
+     * @return {@code true} if the callback was registered successfully.
+     */
+    @Override
+    public boolean registerBackgroundActivityStartCallback(IBinder callback) {
+        return mTaskSupervisor.getBackgroundActivityLaunchController()
+                .addStrictModeCallback(Binder.getCallingUid(), callback);
+    }
+
+    /**
+     * Unregisters an app that uses the Strict Mode for detecting BAL.
+     *
+     * @param callback the callback to unregister
+     */
+    @Override
+    public void unregisterBackgroundActivityStartCallback(IBinder callback) {
+        mTaskSupervisor.getBackgroundActivityLaunchController()
+                .removeStrictModeCallback(Binder.getCallingUid(), callback);
+    }
+
+    /**
      * Wrap the {@link ActivityOptions} in {@link SafeActivityOptions} and attach caller options
      * that allow using the callers permissions to start background activities.
      */
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index f1dd41e..90c0866 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -37,6 +37,7 @@
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
 import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.AppCompatUtils.isChangeEnabled;
+import static com.android.server.wm.AppCompatUtils.isDisplayIgnoreActivitySizeRestrictions;
 
 import android.annotation.NonNull;
 import android.content.pm.IPackageManager;
@@ -175,8 +176,17 @@
                 && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest();
     }
 
+    /**
+     * Whether to ignore fixed orientation, aspect ratio and resizability of activity.
+     */
     boolean hasFullscreenOverride() {
-        return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled();
+        return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled()
+                || shouldIgnoreActivitySizeRestrictionsForDisplay();
+    }
+
+    boolean shouldIgnoreActivitySizeRestrictionsForDisplay() {
+        return isDisplayIgnoreActivitySizeRestrictions(mActivityRecord)
+                && !mAllowOrientationOverrideOptProp.isFalse();
     }
 
     float getUserMinAspectRatio() {
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index e3a9d67..7aed33d 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -53,11 +53,16 @@
 
     @ActivityInfo.ScreenOrientation
     int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) {
+        final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                mAppCompatOverrides.getAppCompatAspectRatioOverrides();
+        // Ignore all orientation requests of activities for eligible virtual displays.
+        if (aspectRatioOverrides.shouldIgnoreActivitySizeRestrictionsForDisplay()) {
+            return SCREEN_ORIENTATION_USER;
+        }
         final DisplayContent displayContent = mActivityRecord.mDisplayContent;
         final boolean isIgnoreOrientationRequestEnabled = displayContent != null
                 && displayContent.getIgnoreOrientationRequest();
-        final boolean hasFullscreenOverride = mAppCompatOverrides
-                .getAppCompatAspectRatioOverrides().hasFullscreenOverride();
+        final boolean hasFullscreenOverride = aspectRatioOverrides.hasFullscreenOverride();
         final boolean shouldCameraCompatControlOrientation =
                 AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
         if (hasFullscreenOverride && isIgnoreOrientationRequestEnabled
@@ -76,8 +81,8 @@
         // In some cases (e.g. Kids app) we need to map the candidate orientation to some other
         // orientation.
         candidate = mActivityRecord.mWmService.mapOrientationRequest(candidate);
-        final boolean shouldApplyUserMinAspectRatioOverride = mAppCompatOverrides
-                .getAppCompatAspectRatioOverrides().shouldApplyUserMinAspectRatioOverride();
+        final boolean shouldApplyUserMinAspectRatioOverride = aspectRatioOverrides
+                .shouldApplyUserMinAspectRatioOverride();
         if (shouldApplyUserMinAspectRatioOverride && (!isFixedOrientation(candidate)
                 || candidate == SCREEN_ORIENTATION_LOCKED)) {
             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate)
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index f069dcd..d0d3d43 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -149,7 +149,7 @@
             @NonNull Configuration newParentConfig) {
         mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy()
                 .findOpaqueNotFinishingActivityBelow()
-                .map(activityRecord -> mSizeCompatScale)
+                .map(ar -> Math.min(1.0f, ar.getCompatScale()))
                 .orElseGet(() -> calculateSizeCompatScale(
                         resolvedAppBounds, containerAppBounds, newParentConfig));
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 8d84248..db76eb9 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -32,6 +32,8 @@
 import android.view.InsetsState;
 import android.view.WindowInsets;
 
+import com.android.window.flags.Flags;
+
 import java.util.function.BooleanSupplier;
 
 /**
@@ -86,10 +88,22 @@
     /**
      * @param activityRecord The {@link ActivityRecord} for the app package.
      * @param overrideChangeId The per-app override identifier.
-     * @return {@code true} if the per-app override is enable for the given activity.
+     * @return {@code true} if the per-app override is enable for the given activity and the
+     * display does not ignore fixed orientation, aspect ratio and resizability of activity.
      */
     static boolean isChangeEnabled(@NonNull ActivityRecord activityRecord, long overrideChangeId) {
-        return activityRecord.info.isChangeEnabled(overrideChangeId);
+        return activityRecord.info.isChangeEnabled(overrideChangeId)
+                && !isDisplayIgnoreActivitySizeRestrictions(activityRecord);
+    }
+
+    /**
+     * Whether the display ignores fixed orientation, aspect ratio and resizability of activities.
+     */
+    static boolean isDisplayIgnoreActivitySizeRestrictions(
+            @NonNull ActivityRecord activityRecord) {
+        final DisplayContent dc = activityRecord.mDisplayContent;
+        return Flags.vdmForceAppUniversalResizableApi() && dc != null
+                && dc.isDisplayIgnoreActivitySizeRestrictions();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 5c53c2a..2fe023e 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -107,6 +107,12 @@
         mNavigationMonitor.onFocusWindowChanged(newFocus);
     }
 
+    void onEmbeddedWindowGestureTransferred(@NonNull WindowState host) {
+        if (Flags.disallowAppProgressEmbeddedWindow()) {
+            mNavigationMonitor.onEmbeddedWindowGestureTransferred(host);
+        }
+    }
+
     /**
      * Set up the necessary leashes and build a {@link BackNavigationInfo} instance for an upcoming
      * back gesture animation.
@@ -178,6 +184,9 @@
                 return null;
             }
 
+            final ArrayList<EmbeddedWindowController.EmbeddedWindow> embeddedWindows = wmService
+                    .mEmbeddedWindowController.getByHostWindow(window);
+
             currentActivity = window.mActivityRecord;
             currentTask = window.getTask();
             if ((currentTask != null && !currentTask.isVisibleRequested())
@@ -199,11 +208,22 @@
             infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
             infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
             infoBuilder.setTouchableRegion(window.getFrame());
-            infoBuilder.setAppProgressAllowed((window.getAttrs().privateFlags
-                    & PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0);
             if (currentTask != null) {
                 infoBuilder.setFocusedTaskId(currentTask.mTaskId);
             }
+            boolean transferGestureToEmbedded = false;
+            if (Flags.disallowAppProgressEmbeddedWindow() && embeddedWindows != null) {
+                for (int i = embeddedWindows.size() - 1; i >= 0; --i) {
+                    if (embeddedWindows.get(i).mGestureToEmbedded) {
+                        transferGestureToEmbedded = true;
+                        break;
+                    }
+                }
+            }
+            final boolean canInterruptInView = (window.getAttrs().privateFlags
+                    & PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0;
+            infoBuilder.setAppProgressAllowed(canInterruptInView && !transferGestureToEmbedded
+                    && callbackInfo.isAnimationCallback());
             mNavigationMonitor.startMonitor(window, navigationObserver);
 
             ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
@@ -742,6 +762,20 @@
         }
 
         /**
+         * Notify focus window has transferred touch gesture to embedded window. Shell should pilfer
+         * pointers so embedded process won't receive motion event.
+         *
+         */
+        void onEmbeddedWindowGestureTransferred(@NonNull WindowState host) {
+            if (!isMonitorForRemote() || host != mNavigatingWindow) {
+                return;
+            }
+            final Bundle result = new Bundle();
+            result.putBoolean(BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED, true);
+            mObserver.sendResult(result);
+        }
+
+        /**
          * Notify an unexpected transition has happened during back navigation.
          */
         private void onTransitionReadyWhileNavigate(ArrayList<WindowContainer> opening,
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 9c7a6f9..1933408 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -52,6 +52,7 @@
 import static com.android.window.flags.Flags.balRequireOptInSameUid;
 import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid;
 import static com.android.window.flags.Flags.balShowToastsBlocked;
+import static com.android.window.flags.Flags.balStrictModeRo;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 import static java.util.Objects.requireNonNull;
@@ -63,6 +64,7 @@
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
 import android.app.BackgroundStartPrivileges;
+import android.app.IBackgroundActivityLaunchCallback;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -70,13 +72,17 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.IBinder;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.DebugUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.widget.Toast;
 
 import com.android.internal.R;
@@ -91,6 +97,7 @@
 import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.StringJoiner;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -141,6 +148,10 @@
     private final ActivityTaskManagerService mService;
 
     private final ActivityTaskSupervisor mSupervisor;
+    @GuardedBy("mStrictModeBalCallbacks")
+    private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>>
+            mStrictModeBalCallbacks = new SparseArray<>();
+
 
     // TODO(b/263368846) Rename when ASM logic is moved in
     @Retention(SOURCE)
@@ -839,7 +850,120 @@
             // only show a toast if either caller or real caller could launch if they opted in
             showToast("BAL blocked. goo.gle/android-bal");
         }
-        return statsLog(BalVerdict.BLOCK, state);
+        BalVerdict verdict = statsLog(BalVerdict.BLOCK, state);
+        if (balStrictModeRo()) {
+            String abortDebugMessage;
+            if (state.isPendingIntent()) {
+                abortDebugMessage =
+                        "PendingIntent Activity start blocked in " + state.mRealCallingPackage
+                                + ". "
+                                + "PendingIntent was created in " + state.mCallingPackage
+                                + ". "
+                                + (state.mResultForRealCaller.allows()
+                                ? state.mRealCallingPackage
+                                + " could opt in to grant BAL privileges when sending. "
+                                : "")
+                                + (state.mResultForCaller.allows()
+                                ? state.mCallingPackage
+                                + " could opt in to grant BAL privileges when creating."
+                                : "")
+                                + "The intent would have started " + state.mIntent.getComponent();
+            } else {
+                abortDebugMessage = "Activity start blocked. "
+                        + "The intent would have started " + state.mIntent.getComponent();
+            }
+            strictModeLaunchAborted(state.mCallingUid, abortDebugMessage);
+            if (!state.callerIsRealCaller()) {
+                strictModeLaunchAborted(state.mRealCallingUid, abortDebugMessage);
+            }
+        }
+        return verdict;
+    }
+
+    /**
+     * Retrieve a registered strict mode callback for BAL.
+     * @param uid the uid of the app.
+     * @return the callback if it exists, returns <code>null</code> otherwise.
+     */
+    @Nullable
+    Map<IBinder, IBackgroundActivityLaunchCallback> getStrictModeBalCallbacks(int uid) {
+        ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap;
+        synchronized (mStrictModeBalCallbacks) {
+            callbackMap =
+                    mStrictModeBalCallbacks.get(uid);
+            if (callbackMap == null) {
+                return null;
+            }
+            return new ArrayMap<>(callbackMap);
+        }
+    }
+
+    /**
+     * Add strict mode callback for BAL.
+     *
+     * @param uid      the UID for which the binder is registered.
+     * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is
+     *                 blocked.
+     * @return {@code true} if the callback has been successfully added.
+     */
+    boolean addStrictModeCallback(int uid, IBinder callback) {
+        IBackgroundActivityLaunchCallback balCallback =
+                IBackgroundActivityLaunchCallback.Stub.asInterface(callback);
+        synchronized (mStrictModeBalCallbacks) {
+            ArrayMap<IBinder, IBackgroundActivityLaunchCallback> callbackMap =
+                    mStrictModeBalCallbacks.get(uid);
+            if (callbackMap == null) {
+                callbackMap = new ArrayMap<>();
+                mStrictModeBalCallbacks.put(uid, callbackMap);
+            }
+            if (callbackMap.containsKey(callback)) {
+                return false;
+            }
+            callbackMap.put(callback, balCallback);
+        }
+        try {
+            callback.linkToDeath(() -> removeStrictModeCallback(uid, callback), 0);
+        } catch (RemoteException e) {
+            removeStrictModeCallback(uid, callback);
+        }
+        return true;
+    }
+
+    /**
+     * Remove strict mode callback for BAL.
+     *
+     * @param uid      the UID for which the binder is registered.
+     * @param callback the {@link IBackgroundActivityLaunchCallback} binder to call when BAL is
+     *                 blocked.
+     */
+    void removeStrictModeCallback(int uid, IBinder callback) {
+        synchronized (mStrictModeBalCallbacks) {
+            Map<IBinder, IBackgroundActivityLaunchCallback> callbackMap =
+                    mStrictModeBalCallbacks.get(uid);
+            if (callback == null || !callbackMap.containsKey(callback)) {
+                return;
+            }
+            callbackMap.remove(callback);
+            if (callbackMap.isEmpty()) {
+                mStrictModeBalCallbacks.remove(uid);
+            }
+        }
+    }
+
+    private void strictModeLaunchAborted(int callingUid, String message) {
+        Map<IBinder, IBackgroundActivityLaunchCallback> strictModeBalCallbacks =
+                getStrictModeBalCallbacks(callingUid);
+        if (strictModeBalCallbacks == null) {
+            return;
+        }
+        for (Map.Entry<IBinder, IBackgroundActivityLaunchCallback> callbackEntry :
+                strictModeBalCallbacks.entrySet()) {
+            try {
+                callbackEntry.getValue().onBackgroundActivityLaunchAborted(message);
+            } catch (RemoteException e) {
+                removeStrictModeCallback(callingUid, callbackEntry.getKey());
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index aac756f..e827f44 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5789,6 +5789,17 @@
     }
 
     /**
+     * Checks if this display is allowed to ignore fixed orientation, aspect ratio,
+     * and resizability of apps.
+     *
+     * <p>This can be set via
+     * {@link VirtualDisplayConfig.Builder#setIgnoreActivitySizeRestrictions}.</p>
+     */
+    boolean isDisplayIgnoreActivitySizeRestrictions() {
+        return mWmService.mDisplayWindowSettings.isIgnoreActivitySizeRestrictionsLocked(this);
+    }
+
+    /**
      * The direct child layer of the display to put all non-overlay windows. This is also used for
      * screen rotation animation so that there is a parent layer to put the animation leash.
      */
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index e585efa..c87b811 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -275,6 +275,24 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
+    boolean isIgnoreActivitySizeRestrictionsLocked(@NonNull DisplayContent dc) {
+        final DisplayInfo displayInfo = dc.getDisplayInfo();
+        final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
+        return settings.mIgnoreActivitySizeRestrictions != null
+                && settings.mIgnoreActivitySizeRestrictions;
+    }
+
+    void setIgnoreActivitySizeRestrictionsOnDisplayLocked(@NonNull String displayUniqueId,
+            int displayType, boolean enabled) {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.uniqueId = displayUniqueId;
+        displayInfo.type = displayType;
+        final SettingsProvider.SettingsEntry overrideSettings =
+                mSettingsProvider.getOverrideSettings(displayInfo);
+        overrideSettings.mIgnoreActivitySizeRestrictions = enabled;
+        mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+    }
+
     void clearDisplaySettings(@NonNull String displayUniqueId, int displayType) {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.uniqueId = displayUniqueId;
@@ -474,6 +492,8 @@
             Boolean mIgnoreDisplayCutout;
             @Nullable
             Boolean mDontMoveToTop;
+            @Nullable
+            Boolean mIgnoreActivitySizeRestrictions;
 
             SettingsEntry() {}
 
@@ -557,6 +577,11 @@
                     mDontMoveToTop = other.mDontMoveToTop;
                     changed = true;
                 }
+                if (!Objects.equals(other.mIgnoreActivitySizeRestrictions,
+                        mIgnoreActivitySizeRestrictions)) {
+                    mIgnoreActivitySizeRestrictions = other.mIgnoreActivitySizeRestrictions;
+                    changed = true;
+                }
                 return changed;
             }
 
@@ -649,6 +674,11 @@
                     mDontMoveToTop = delta.mDontMoveToTop;
                     changed = true;
                 }
+                if (delta.mIgnoreActivitySizeRestrictions != null && !Objects.equals(
+                        delta.mIgnoreActivitySizeRestrictions, mIgnoreActivitySizeRestrictions)) {
+                    mIgnoreActivitySizeRestrictions = delta.mIgnoreActivitySizeRestrictions;
+                    changed = true;
+                }
                 return changed;
             }
 
@@ -667,7 +697,8 @@
                         && mFixedToUserRotation == null
                         && mIgnoreOrientationRequest == null
                         && mIgnoreDisplayCutout == null
-                        && mDontMoveToTop == null;
+                        && mDontMoveToTop == null
+                        && mIgnoreActivitySizeRestrictions == null;
             }
 
             @Override
@@ -691,7 +722,9 @@
                         && Objects.equals(mFixedToUserRotation, that.mFixedToUserRotation)
                         && Objects.equals(mIgnoreOrientationRequest, that.mIgnoreOrientationRequest)
                         && Objects.equals(mIgnoreDisplayCutout, that.mIgnoreDisplayCutout)
-                        && Objects.equals(mDontMoveToTop, that.mDontMoveToTop);
+                        && Objects.equals(mDontMoveToTop, that.mDontMoveToTop)
+                        && Objects.equals(mIgnoreActivitySizeRestrictions,
+                                that.mIgnoreActivitySizeRestrictions);
             }
 
             @Override
@@ -700,7 +733,7 @@
                         mForcedHeight, mForcedDensity, mForcedScalingMode, mRemoveContentMode,
                         mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mIsHomeSupported,
                         mImePolicy, mFixedToUserRotation, mIgnoreOrientationRequest,
-                        mIgnoreDisplayCutout, mDontMoveToTop);
+                        mIgnoreDisplayCutout, mDontMoveToTop, mIgnoreActivitySizeRestrictions);
             }
 
             @Override
@@ -722,6 +755,7 @@
                         + ", mIgnoreOrientationRequest=" + mIgnoreOrientationRequest
                         + ", mIgnoreDisplayCutout=" + mIgnoreDisplayCutout
                         + ", mDontMoveToTop=" + mDontMoveToTop
+                        + ", mForceAppsUniversalResizable=" + mIgnoreActivitySizeRestrictions
                         + '}';
             }
         }
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 5ac4cf8..907d0dc 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -39,6 +39,8 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.server.input.InputManagerService;
 
+import java.util.ArrayList;
+
 /**
  * Keeps track of embedded windows.
  *
@@ -146,6 +148,20 @@
         return mWindowsByWindowToken.get(windowToken);
     }
 
+    @Nullable ArrayList<EmbeddedWindow> getByHostWindow(WindowState host) {
+        ArrayList<EmbeddedWindow> windows = null;
+        for (int i = mWindows.size() - 1; i >= 0; i--) {
+            final EmbeddedWindow ew = mWindows.valueAt(i);
+            if (ew.mHostWindowState == host) {
+                if (windows == null) {
+                    windows = new ArrayList<>();
+                }
+                windows.add(ew);
+            }
+        }
+        return windows;
+    }
+
     private boolean isValidTouchGestureParams(WindowState hostWindowState,
             EmbeddedWindow embeddedWindow) {
         if (embeddedWindow == null) {
@@ -191,8 +207,12 @@
             throw new SecurityException(
                     "Transfer request must originate from owner of transferFromToken");
         }
-        return mInputManagerService.transferTouchGesture(ew.getInputChannelToken(),
-                transferToHostWindowState.mInputChannelToken);
+        final boolean didTransfer = mInputManagerService.transferTouchGesture(
+                ew.getInputChannelToken(), transferToHostWindowState.mInputChannelToken);
+        if (didTransfer) {
+            ew.mGestureToEmbedded = false;
+        }
+        return didTransfer;
     }
 
     boolean transferToEmbedded(int callingUid, WindowState hostWindowState,
@@ -205,8 +225,15 @@
             throw new SecurityException(
                     "Transfer request must originate from owner of transferFromToken");
         }
-        return mInputManagerService.transferTouchGesture(hostWindowState.mInputChannelToken,
+        final boolean didTransfer = mInputManagerService.transferTouchGesture(
+                hostWindowState.mInputChannelToken,
                 ew.getInputChannelToken());
+        if (didTransfer) {
+            ew.mGestureToEmbedded = true;
+            mAtmService.mBackNavigationController.onEmbeddedWindowGestureTransferred(
+                    hostWindowState);
+        }
+        return didTransfer;
     }
 
     static class EmbeddedWindow implements InputTarget {
@@ -235,6 +262,9 @@
         // the host window.
         private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0;
 
+        /** Whether the gesture is transferred to embedded window. */
+        boolean mGestureToEmbedded = false;
+
         /**
          * @param session  calling session to check ownership of the window
          * @param clientToken client token used to clean up the map if the embedding process dies
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b14dd3f..8aa0530 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3464,7 +3464,8 @@
             return null;
         }
         final Rect windowFrame = mainWindow.getFrame();
-        if (top.getBounds().equals(windowFrame)) {
+        final Rect parentFrame = mainWindow.getParentFrame();
+        if (parentFrame.equals(windowFrame)) {
             return null;
         }
         return windowFrame;
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 82d39a3..ce032b4 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -859,6 +859,18 @@
     public abstract boolean isHomeSupportedOnDisplay(int displayId);
 
     /**
+     * Sets whether the relevant display content ignores fixed orientation, aspect ratio
+     * and resizability of apps.
+     *
+     * @param displayUniqueId The unique ID of the display. Note that the display may not yet be
+     *   created, but whenever it is, this property will be applied.
+     * @param displayType The type of the display, e.g. {@link Display#TYPE_VIRTUAL}.
+     * @param enabled Whether app is universal resizable on this display.
+     */
+    public abstract void setIgnoreActivitySizeRestrictionsOnDisplay(
+            @NonNull String displayUniqueId, int displayType, boolean enabled);
+
+    /**
      * Removes any settings relevant to the given display.
      *
      * <p>This may be used when a property is set for a display unique ID before the display
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 97bf587..88b2d22 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8384,6 +8384,20 @@
         }
 
         @Override
+        public void setIgnoreActivitySizeRestrictionsOnDisplay(@NonNull String displayUniqueId,
+                int displayType, boolean enabled) {
+            final long origId = Binder.clearCallingIdentity();
+            try {
+                synchronized (mGlobalLock) {
+                    mDisplayWindowSettings.setIgnoreActivitySizeRestrictionsOnDisplayLocked(
+                            displayUniqueId, displayType, enabled);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+
+        @Override
         public void clearDisplaySettings(String displayUniqueId, int displayType) {
             final long origId = Binder.clearCallingIdentity();
             try {
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index 15c9b9f..a4546ae 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -297,6 +297,7 @@
 
         private const val MASK_ANY_FIXED =
             PermissionFlags.USER_SET or
+                PermissionFlags.ONE_TIME or
                 PermissionFlags.USER_FIXED or
                 PermissionFlags.POLICY_FIXED or
                 PermissionFlags.SYSTEM_FIXED
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index d4b57f1..f439770 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -26,6 +26,8 @@
 import android.net.Uri
 import android.os.Bundle
 import android.os.Parcelable
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.util.ArraySet
 import android.util.SparseArray
 import android.util.SparseIntArray
@@ -47,14 +49,19 @@
 import com.android.server.pm.pkg.AndroidPackage
 import com.android.server.testutils.mockThrowOnUnmocked
 import com.android.server.testutils.whenever
+import org.junit.Rule
 import java.security.KeyPairGenerator
 import java.security.PublicKey
 import java.util.UUID
 import kotlin.contracts.ExperimentalContracts
 
 @ExperimentalContracts
+@EnableFlags(android.content.pm.Flags.FLAG_INCLUDE_FEATURE_FLAGS_IN_PACKAGE_CACHER)
 class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, PackageImpl::class) {
 
+    @get:Rule
+    val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     companion object {
         private val TEST_UUID = UUID.fromString("57554103-df3e-4475-ae7a-8feba49353ac")
     }
@@ -93,6 +100,8 @@
         "getUsesOptionalLibrariesSorted",
         "getUsesSdkLibrariesSorted",
         "getUsesStaticLibrariesSorted",
+        "readFeatureFlagState",
+        "writeFeatureFlagState",
         // Tested through setting minor/major manually
         "setLongVersionCode",
         "getLongVersionCode",
@@ -149,6 +158,10 @@
         "isSystem",
         "isSystemExt",
         "isVendor",
+
+        // Tested through addFeatureFlag
+        "addFeatureFlag",
+        "getFeatureFlagState",
     )
 
     override val baseParams = listOf(
@@ -613,6 +626,9 @@
         .setSplitClassLoaderName(1, "testSplitClassLoaderNameOne")
         .addUsesSdkLibrary("testSdk", 2L, arrayOf("testCertDigest1"), true)
         .addUsesStaticLibrary("testStatic", 3L, arrayOf("testCertDigest2"))
+        .addFeatureFlag("testFlag1", null)
+        .addFeatureFlag("testFlag2", true)
+        .addFeatureFlag("testFlag3", false)
 
     override fun finalizeObject(parcelable: Parcelable) {
         (parcelable as PackageImpl).hideAsParsed().hideAsFinal()
@@ -673,6 +689,12 @@
                 .containsExactly("testCertDigest2")
 
         expect.that(after.storageUuid).isEqualTo(TEST_UUID)
+
+        expect.that(after.featureFlagState).containsExactlyEntriesIn(mapOf(
+            "testFlag1" to null,
+            "testFlag2" to true,
+            "testFlag3" to false,
+        ))
     }
 
     private fun testKey() = KeyPairGenerator.getInstance("RSA")
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
index 574f369..ae404cf 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY;
 import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY;
+import static com.android.server.pm.BackgroundInstallControlCallbackHelper.INSTALL_EVENT_TYPE_KEY;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.after;
@@ -84,12 +85,18 @@
         int testUserId = 1;
         mCallbackHelper.registerBackgroundInstallCallback(mCallback);
 
-        mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName);
+        mCallbackHelper.notifyAllCallbacks(
+                testUserId,
+                testPackageName,
+                BackgroundInstallControlService.INSTALL_EVENT_TYPE_INSTALL);
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture());
         Bundle receivedBundle = bundleCaptor.getValue();
         assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY));
         assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY));
+        assertEquals(
+                BackgroundInstallControlService.INSTALL_EVENT_TYPE_INSTALL,
+                receivedBundle.getInt(INSTALL_EVENT_TYPE_KEY));
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 7481fc8..2edde9b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -1615,7 +1615,8 @@
                 List.of(tile)
         );
 
-        assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()).doesNotContain(tile);
+        assertThat(mA11yms.getCurrentUserState()
+                .getShortcutTargetsLocked(QUICK_SETTINGS)).doesNotContain(tile.flattenToString());
     }
 
     @Test
@@ -1636,7 +1637,7 @@
                 List.of(tile)
         );
 
-        assertThat(mA11yms.getCurrentUserState().getA11yQsTargets())
+        assertThat(mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS))
                 .contains(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString());
     }
 
@@ -1656,7 +1657,7 @@
         );
 
         assertThat(
-                mA11yms.getCurrentUserState().getA11yQsTargets()
+                mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS)
         ).containsExactlyElementsIn(List.of(
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(),
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString())
@@ -1676,7 +1677,7 @@
         );
 
         assertThat(
-                mA11yms.getCurrentUserState().getA11yQsTargets()
+                mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS)
         ).doesNotContain(
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString());
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index 627b5e3..8c35925 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -29,6 +29,7 @@
 import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
@@ -72,6 +73,7 @@
 import com.android.internal.accessibility.AccessibilityShortcutController;
 import com.android.internal.accessibility.common.ShortcutConstants;
 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
+import com.android.internal.accessibility.util.ShortcutUtils;
 import com.android.internal.util.test.FakeSettingsProvider;
 
 import org.junit.After;
@@ -454,17 +456,7 @@
 
         mUserState.updateShortcutTargetsLocked(newTargets, QUICK_SETTINGS);
 
-        assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets);
-    }
-
-    @Test
-    public void getA11yQsTargets_returnsCopiedData() {
-        updateShortcutTargetsLocked_quickSettings_valueUpdated();
-
-        Set<String> targets = mUserState.getA11yQsTargets();
-        targets.clear();
-
-        assertThat(mUserState.getA11yQsTargets()).isNotEmpty();
+        assertThat(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS)).isEqualTo(newTargets);
     }
 
     @Test
@@ -539,6 +531,31 @@
         assertThat(mUserState.isShortcutMagnificationEnabledLocked()).isFalse();
     }
 
+    @Test
+    public void getShortcutTargetsLocked_returnsCorrectTargets() {
+        for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) {
+            if (((TRIPLETAP | TWOFINGER_DOUBLETAP) & shortcutType) == shortcutType) {
+                continue;
+            }
+            Set<String> expectedSet = Set.of(ShortcutUtils.convertToKey(shortcutType));
+            mUserState.updateShortcutTargetsLocked(expectedSet, shortcutType);
+
+            assertThat(mUserState.getShortcutTargetsLocked(shortcutType))
+                    .containsExactlyElementsIn(expectedSet);
+        }
+    }
+
+    @Test
+    public void getShortcutTargetsLocked_returnsCopiedData() {
+        Set<String> set = Set.of("FOO", "BAR");
+        mUserState.updateShortcutTargetsLocked(set, SOFTWARE);
+
+        Set<String> targets = mUserState.getShortcutTargetsLocked(ALL);
+        targets.clear();
+
+        assertThat(mUserState.getShortcutTargetsLocked(ALL)).isNotEmpty();
+    }
+
     private int getSecureIntForUser(String key, int userId) {
         return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 2f7b8d2..4ef37b9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -69,6 +69,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.security.KeyStoreAuthorization;
 import android.testing.TestableContext;
@@ -119,6 +120,7 @@
     @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver;
     @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger;
     @Mock private BiometricCameraManager mBiometricCameraManager;
+    @Mock private UserManager mUserManager;
     @Mock private BiometricManager mBiometricManager;
 
     private Random mRandom;
@@ -846,7 +848,8 @@
                 TEST_PACKAGE,
                 checkDevicePolicyManager,
                 mContext,
-                mBiometricCameraManager);
+                mBiometricCameraManager,
+                mUserManager);
     }
 
     private AuthSession createAuthSession(List<BiometricSensor> sensors,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index 85e45f4..510dd4d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -39,6 +39,7 @@
 import android.hardware.biometrics.IBiometricAuthenticator;
 import android.hardware.biometrics.PromptInfo;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -64,6 +65,8 @@
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
 
+    private static final int USER_ID = 0;
+    private static final int OWNER_ID = 10;
     private static final int SENSOR_ID_FINGERPRINT = 0;
     private static final int SENSOR_ID_FACE = 1;
     private static final String TEST_PACKAGE_NAME = "PreAuthInfoTestPackage";
@@ -84,6 +87,8 @@
     BiometricService.SettingObserver mSettingObserver;
     @Mock
     BiometricCameraManager mBiometricCameraManager;
+    @Mock
+    UserManager mUserManager;
 
     @Before
     public void setup() throws RemoteException {
@@ -118,9 +123,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).isEmpty();
     }
@@ -134,9 +139,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(1);
     }
@@ -151,9 +156,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(0);
     }
@@ -171,9 +176,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(faceSensor, fingerprintSensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID,
+                promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext,
+                mBiometricCameraManager, mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(0);
         assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
@@ -191,9 +196,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(faceSensor, fingerprintSensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID,
+                promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext,
+                mBiometricCameraManager, mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(1);
         assertThat(preAuthInfo.eligibleSensors.get(0).modality).isEqualTo(TYPE_FINGERPRINT);
@@ -209,8 +214,9 @@
         final PromptInfo promptInfo = new PromptInfo();
         promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(1);
     }
@@ -224,8 +230,9 @@
         final PromptInfo promptInfo = new PromptInfo();
         promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(0);
     }
@@ -241,8 +248,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK
                 | BiometricManager.Authenticators.BIOMETRIC_STRONG);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(1);
     }
@@ -257,8 +265,9 @@
         final PromptInfo promptInfo = new PromptInfo();
         promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
 
         assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
                 BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE);
@@ -279,9 +288,9 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
         promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
         PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(faceSensor, fingerprintSensor),
-                0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID,
+                promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext,
+                mBiometricCameraManager, mUserManager);
 
         assertThat(preAuthInfo.eligibleSensors).hasSize(0);
         assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
@@ -299,11 +308,30 @@
         promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
-                mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
-                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+                mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
         assertThat(promptInfo.getNegativeButtonText()).isEqualTo(TEST_PACKAGE_NAME);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_PRIVATE_SPACE_BP)
+    public void testCredentialOwnerIdAsUserId() throws Exception {
+        when(mUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(OWNER_ID);
+
+        final BiometricSensor sensor = getFaceSensor();
+        final PromptInfo promptInfo = new PromptInfo();
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
+        promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
+        final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
+                mSettingObserver, List.of(sensor), USER_ID , promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
+
+        assertThat(preAuthInfo.userId).isEqualTo(OWNER_ID);
+        assertThat(preAuthInfo.callingUserId).isEqualTo(USER_ID);
+    }
+
     private BiometricSensor getFingerprintSensor() {
         BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FINGERPRINT,
                 TYPE_FINGERPRINT, BiometricManager.Authenticators.BIOMETRIC_STRONG,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 3360e1d..c741c6c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -37,6 +37,7 @@
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
@@ -2676,6 +2677,60 @@
     }
 
     @Test
+    public void onActiveSourceLost_oneTouchPlay_noStandbyAfterTimeout() {
+        mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
+                HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
+                HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
+        mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress,
+                mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest");
+        mPowerManager.setInteractive(true);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+        HdmiCecMessage activeSourceFromPlayback =
+                HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                        mPlaybackPhysicalAddress);
+
+        assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(activeSourceFromTv))
+                .isEqualTo(Constants.HANDLED);
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(ADDR_TV);
+        mTestLooper.dispatchAll();
+
+        // Pop-up is triggered.
+        mTestLooper.moveTimeForward(POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
+        mTestLooper.dispatchAll();
+        // RequestActiveSourceAction is triggered and TV confirms active source.
+        mNativeWrapper.onCecMessage(activeSourceFromTv);
+        mTestLooper.dispatchAll();
+
+        assertThat(mIsOnActiveSourceLostPopupActive).isTrue();
+        mHdmiControlService.oneTouchPlay(new IHdmiControlCallback() {
+            @Override
+            public void onComplete(int result) throws RemoteException {
+            }
+
+            @Override
+            public IBinder asBinder() {
+                return null;
+            }
+        });
+        mTestLooper.dispatchAll();
+
+        assertThat(mIsOnActiveSourceLostPopupActive).isFalse();
+        assertThat(mNativeWrapper.getResultMessages().contains(activeSourceFromPlayback)).isTrue();
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress)
+                .isEqualTo(mPlaybackLogicalAddress);
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress)
+                .isEqualTo(mPlaybackPhysicalAddress);
+        mTestLooper.moveTimeForward(STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPowerManager.isInteractive()).isTrue();
+    }
+
+    @Test
     public void handleRoutingChange_addressNotAllocated_removeActiveSourceAction() {
         long allocationDelay = TimeUnit.SECONDS.toMillis(60);
         mHdmiCecLocalDevicePlayback.mPlaybackDeviceActionOnRoutingControl =
diff --git a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
index a222ef0..5852af7 100644
--- a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.tv.tunerresourcemanager;
 
+import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.spy;
@@ -39,7 +41,9 @@
 import android.media.tv.tunerresourcemanager.TunerLnbRequest;
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
 import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -47,6 +51,7 @@
 import com.google.common.truth.Correspondence;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -70,6 +75,8 @@
     private TunerResourceManagerService mTunerResourceManagerService;
     private boolean mIsForeground;
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private final class TunerClient extends IResourcesReclaimListener.Stub {
         int[] mClientId;
         ClientProfile mProfile;
@@ -125,19 +132,6 @@
         }
     }
 
-    private static final class TestResourcesReclaimListener extends IResourcesReclaimListener.Stub {
-        boolean mReclaimed;
-
-        @Override
-        public void onReclaimResources() {
-            mReclaimed = true;
-        }
-
-        public boolean isReclaimed() {
-            return mReclaimed;
-        }
-    }
-
     // A correspondence to compare a FrontendResource and a TunerFrontendInfo.
     private static final Correspondence<FrontendResource, TunerFrontendInfo> FR_TFI_COMPARE =
             Correspondence.from((FrontendResource actual, TunerFrontendInfo expected) -> {
@@ -485,6 +479,62 @@
     }
 
     @Test
+    @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN})
+    public void requestFrontendTest_NoFrontendAvailable_RequestWithEqualPriority()
+            throws RemoteException {
+        // Register clients
+        TunerClient client0 = new TunerClient();
+        TunerClient client1 = new TunerClient();
+        client0.register(
+                "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+        client1.register(
+                "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+
+        // Init frontend resource.
+        TunerFrontendInfo[] infos = new TunerFrontendInfo[1];
+        infos[0] =
+                tunerFrontendInfo(0 /*handle*/, FrontendSettings.TYPE_DVBT, 1 /*exclusiveGroupId*/);
+        mTunerResourceManagerService.setFrontendInfoListInternal(infos);
+
+        // client0 requests for 1 frontend
+        TunerFrontendRequest request =
+                tunerFrontendRequest(client0.getId() /*clientId*/, FrontendSettings.TYPE_DVBT);
+        long[] frontendHandle = new long[1];
+        assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle))
+                .isTrue();
+        assertThat(frontendHandle[0]).isEqualTo(infos[0].handle);
+        assertThat(client0.getProfile().getInUseFrontendHandles())
+                .isEqualTo(new HashSet<Long>(Arrays.asList(infos[0].handle)));
+
+        // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder
+        // (client0) to maintain ownership such as requester will not get the resources.
+        client1.getProfile().setResourceHolderRetain(true);
+
+        request = tunerFrontendRequest(client1.getId() /*clientId*/, FrontendSettings.TYPE_DVBT);
+        assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle))
+                .isFalse();
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle).isInUse())
+                .isTrue();
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle)
+                           .getOwnerClientId())
+                .isEqualTo(client0.getId());
+        assertThat(client0.isReclaimed()).isFalse();
+
+        // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource
+        // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the
+        // resources.
+        client1.getProfile().setResourceHolderRetain(false);
+
+        assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle))
+                .isTrue();
+        assertThat(frontendHandle[0]).isEqualTo(infos[0].handle);
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle)
+                           .getOwnerClientId())
+                .isEqualTo(client1.getId());
+        assertThat(client0.isReclaimed()).isTrue();
+    }
+
+    @Test
     public void releaseFrontendTest_UnderTheSameExclusiveGroup() throws RemoteException {
         // Register clients
         TunerClient client0 = new TunerClient();
@@ -565,6 +615,74 @@
     }
 
     @Test
+    @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN})
+    public void requestCasTest_NoCasAvailable_RequestWithEqualPriority() throws RemoteException {
+        // Register clients
+        TunerClient client0 = new TunerClient();
+        TunerClient client1 = new TunerClient();
+        client0.register(
+                "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+        client1.register(
+                "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+
+        // Init cas resources.
+        mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/);
+
+        CasSessionRequest request = casSessionRequest(client0.getId(), 1 /*casSystemId*/);
+        long[] casSessionHandle = new long[1];
+
+        // client0 requests for 2 cas sessions.
+        assertThat(
+                mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle))
+                .isTrue();
+        assertThat(
+                mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle))
+                .isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(1);
+        assertThat(client0.getProfile().getInUseCasSystemId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId())));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue();
+
+        // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder
+        // to maintain ownership such as requester (client1) will not get the resources.
+        client1.getProfile().setResourceHolderRetain(true);
+
+        request = casSessionRequest(client1.getId(), 1);
+        assertThat(
+                mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle))
+                .isFalse();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(-1);
+        assertThat(client0.getProfile().getInUseCasSystemId()).isEqualTo(1);
+        assertThat(client1.getProfile().getInUseCasSystemId())
+                .isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId())));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue();
+        assertThat(client0.isReclaimed()).isFalse();
+
+        // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource
+        // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the
+        // resources.
+        client1.getProfile().setResourceHolderRetain(false);
+
+        assertThat(
+                mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle))
+                .isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0]))
+                .isEqualTo(1);
+        assertThat(client0.getProfile().getInUseCasSystemId())
+                .isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(client1.getProfile().getInUseCasSystemId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client1.getId())));
+        assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse();
+        assertThat(client0.isReclaimed()).isTrue();
+    }
+
+    @Test
     public void requestCiCamTest_NoCiCamAvailable_RequestWithHigherPriority()
             throws RemoteException {
         // Register clients
@@ -612,6 +730,71 @@
     }
 
     @Test
+    @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN})
+    public void requestCiCamTest_NoCiCamAvailable_RequestWithEqualPriority()
+            throws RemoteException {
+        // Register clients
+        TunerClient client0 = new TunerClient();
+        TunerClient client1 = new TunerClient();
+        client0.register(
+                "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+        client1.register(
+                "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+
+        // Init cicam/cas resources.
+        mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/);
+
+        TunerCiCamRequest request = tunerCiCamRequest(client0.getId(), 1 /*ciCamId*/);
+        long[] ciCamHandle = new long[1];
+
+        // client0 request for 2 ciCam sessions.
+        assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle))
+                .isTrue();
+        assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle))
+                .isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0]))
+                .isEqualTo(1);
+        assertThat(client0.getProfile().getInUseCiCamId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId())));
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isTrue();
+
+        // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder
+        // (client0) to maintain ownership such as requester will not get the resources.
+        client1.getProfile().setResourceHolderRetain(true);
+
+        request = tunerCiCamRequest(client1.getId(), 1);
+        assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle))
+                .isFalse();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0]))
+                .isEqualTo(-1);
+        assertThat(client0.getProfile().getInUseCiCamId()).isEqualTo(1);
+        assertThat(client1.getProfile().getInUseCiCamId())
+                .isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId())));
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isTrue();
+        assertThat(client0.isReclaimed()).isFalse();
+
+        // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource
+        // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the
+        // resources.
+        client1.getProfile().setResourceHolderRetain(false);
+
+        assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle))
+                .isTrue();
+        assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0]))
+                .isEqualTo(1);
+        assertThat(client0.getProfile().getInUseCiCamId())
+                .isEqualTo(ClientProfile.INVALID_RESOURCE_ID);
+        assertThat(client1.getProfile().getInUseCiCamId()).isEqualTo(1);
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds())
+                .isEqualTo(new HashSet<Integer>(Arrays.asList(client1.getId())));
+        assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isFalse();
+        assertThat(client0.isReclaimed()).isTrue();
+    }
+
+    @Test
     public void releaseCasTest() throws RemoteException {
         // Register clients
         TunerClient client0 = new TunerClient();
@@ -721,6 +904,59 @@
     }
 
     @Test
+    @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN})
+    public void requestLnbTest_NoLnbAvailable_RequestWithEqualPriority() throws RemoteException {
+        // Register clients
+        TunerClient client0 = new TunerClient();
+        TunerClient client1 = new TunerClient();
+        client0.register(
+                "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+        client1.register(
+                "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100);
+
+        // Init lnb resources.
+        long[] lnbHandles = {1};
+        mTunerResourceManagerService.setLnbInfoListInternal(lnbHandles);
+
+        // client0 requests 1 lnb
+        TunerLnbRequest request = new TunerLnbRequest();
+        request.clientId = client0.getId();
+        long[] lnbHandle = new long[1];
+        assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isTrue();
+        assertThat(lnbHandle[0]).isEqualTo(lnbHandles[0]);
+        assertThat(client0.getProfile().getInUseLnbHandles())
+                .isEqualTo(new HashSet<Long>(Arrays.asList(lnbHandles[0])));
+
+        // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder
+        // (client0) to maintain ownership such as requester will not get the resources.
+        client1.getProfile().setResourceHolderRetain(true);
+
+        request = new TunerLnbRequest();
+        request.clientId = client1.getId();
+
+        assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isFalse();
+        assertThat(lnbHandle[0]).isNotEqualTo(lnbHandles[0]);
+        assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).isInUse()).isTrue();
+        assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).getOwnerClientId())
+                .isEqualTo(client0.getId());
+        assertThat(client0.isReclaimed()).isFalse();
+        assertThat(client1.getProfile().getInUseLnbHandles().size()).isEqualTo(0);
+
+        // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource
+        // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the
+        // resources.
+        client1.getProfile().setResourceHolderRetain(false);
+
+        assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isTrue();
+        assertThat(lnbHandle[0]).isEqualTo(lnbHandles[0]);
+        assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).isInUse()).isTrue();
+        assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).getOwnerClientId())
+                .isEqualTo(client1.getId());
+        assertThat(client0.isReclaimed()).isTrue();
+        assertThat(client0.getProfile().getInUseLnbHandles().size()).isEqualTo(0);
+    }
+
+    @Test
     public void releaseLnbTest() throws RemoteException {
         // Register clients
         TunerClient client0 = new TunerClient();
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index def3355..aee9f0f 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -36,18 +36,15 @@
     Map<String, Map<Integer, PackageInfo>> mPackages = new HashMap();
     private final int mNumRelros;
     private final boolean mIsDebuggable;
-    private int mMultiProcessSetting;
-    private final boolean mMultiProcessDefault;
 
     public static final int PRIMARY_USER_ID = 0;
 
-    public TestSystemImpl(WebViewProviderInfo[] packageConfigs, int numRelros, boolean isDebuggable,
-            boolean multiProcessDefault) {
+    public TestSystemImpl(WebViewProviderInfo[] packageConfigs, int numRelros,
+            boolean isDebuggable) {
         mPackageConfigs = packageConfigs;
         mNumRelros = numRelros;
         mIsDebuggable = isDebuggable;
         mUsers.add(PRIMARY_USER_ID);
-        mMultiProcessDefault = multiProcessDefault;
     }
 
     public void addUser(int userId) {
@@ -181,26 +178,8 @@
     }
 
     @Override
-    public int getMultiProcessSetting() {
-        return mMultiProcessSetting;
-    }
-
-    @Override
-    public void setMultiProcessSetting(int value) {
-        mMultiProcessSetting = value;
-    }
-
-    @Override
-    public void notifyZygote(boolean enableMultiProcess) {}
-
-    @Override
     public void ensureZygoteStarted() {}
 
     @Override
-    public boolean isMultiProcessDefaultEnabled() {
-        return mMultiProcessDefault;
-    }
-
-    @Override
     public void pinWebviewIfRequired(ApplicationInfo appInfo) {}
 }
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index 06479c8..42eb609 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.webkit;
 
-import static android.webkit.Flags.updateServiceV2;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -27,8 +25,6 @@
 import android.content.pm.Signature;
 import android.os.Build;
 import android.os.Bundle;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Base64;
@@ -62,7 +58,7 @@
 public class WebViewUpdateServiceTest {
     private final static String TAG = WebViewUpdateServiceTest.class.getSimpleName();
 
-    private WebViewUpdateServiceInterface mWebViewUpdateServiceImpl;
+    private WebViewUpdateServiceImpl2 mWebViewUpdateServiceImpl;
     private TestSystemImpl mTestSystemImpl;
 
     private static final String WEBVIEW_LIBRARY_FLAG = "com.android.webview.WebViewLibrary";
@@ -77,38 +73,23 @@
     }
 
     private void setupWithPackages(WebViewProviderInfo[] packages) {
-        setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */,
-                false /* multiProcessDefault */);
+        setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */);
     }
 
     private void setupWithPackagesAndRelroCount(WebViewProviderInfo[] packages, int numRelros) {
-        setupWithAllParameters(packages, numRelros, true /* isDebuggable */,
-                false /* multiProcessDefault */);
+        setupWithAllParameters(packages, numRelros, true /* isDebuggable */);
     }
 
     private void setupWithPackagesNonDebuggable(WebViewProviderInfo[] packages) {
-        setupWithAllParameters(packages, 1 /* numRelros */, false /* isDebuggable */,
-                false /* multiProcessDefault */);
-    }
-
-    private void setupWithPackagesAndMultiProcess(WebViewProviderInfo[] packages,
-            boolean multiProcessDefault) {
-        setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */,
-                multiProcessDefault);
+        setupWithAllParameters(packages, 1 /* numRelros */, false /* isDebuggable */);
     }
 
     private void setupWithAllParameters(WebViewProviderInfo[] packages, int numRelros,
-            boolean isDebuggable, boolean multiProcessDefault) {
-        TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable,
-                multiProcessDefault);
+            boolean isDebuggable) {
+        TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable);
         mTestSystemImpl = Mockito.spy(testing);
-        if (updateServiceV2()) {
-            mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl2(mTestSystemImpl);
-        } else {
-            mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl(mTestSystemImpl);
-        }
+        mWebViewUpdateServiceImpl =
+                new WebViewUpdateServiceImpl2(mTestSystemImpl);
     }
 
     private void setEnabledAndValidPackageInfos(WebViewProviderInfo[] providers) {
@@ -350,24 +331,6 @@
     }
 
     @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    // If the flag is set, will throw an exception because of no available by default provider.
-    public void testEmptyConfig() {
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[0];
-        setupWithPackages(packages);
-        setEnabledAndValidPackageInfos(packages);
-
-        runWebViewBootPreparationOnMainSync();
-
-        Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged(
-                Matchers.anyObject());
-
-        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
-        assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
-        assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
-    }
-
-    @Test
     public void testFailListingEmptyWebviewPackages() {
         String singlePackage = "singlePackage";
         WebViewProviderInfo[] packages = new WebViewProviderInfo[]{
@@ -554,73 +517,6 @@
         }
     }
 
-    /**
-     * Scenario for testing re-enabling a fallback package.
-     */
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    public void testFallbackPackageEnabling() {
-        String testPackage = "testFallback";
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(
-                    testPackage, "", true /* default available */, true /* fallback */, null)};
-        setupWithPackages(packages);
-        mTestSystemImpl.setPackageInfo(
-                createPackageInfo(testPackage, false /* enabled */ , true /* valid */,
-                    true /* installed */));
-
-        // Check that the boot time logic re-enables the fallback package.
-        runWebViewBootPreparationOnMainSync();
-        Mockito.verify(mTestSystemImpl).enablePackageForAllUsers(
-                Mockito.eq(testPackage), Mockito.eq(true));
-
-        // Fake the message about the enabling having changed the package state,
-        // and check we now use that package.
-        mWebViewUpdateServiceImpl.packageStateChanged(
-                testPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
-        checkPreparationPhasesForPackage(testPackage, 1);
-    }
-
-    /**
-     * Scenario for installing primary package when secondary in use.
-     * 1. Start with only secondary installed
-     * 2. Install primary
-     * 3. Primary should be used
-     */
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    // If the flag is set, we don't automitally switch to secondary package unless it is
-    // chosen directly.
-    public void testInstallingPrimaryPackage() {
-        String primaryPackage = "primary";
-        String secondaryPackage = "secondary";
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(
-                    primaryPackage, "", true /* default available */, false /* fallback */, null),
-            new WebViewProviderInfo(
-                    secondaryPackage, "", true /* default available */, false /* fallback */,
-                    null)};
-        setupWithPackages(packages);
-        mTestSystemImpl.setPackageInfo(
-                createPackageInfo(secondaryPackage, true /* enabled */ , true /* valid */,
-                    true /* installed */));
-
-        runWebViewBootPreparationOnMainSync();
-        checkPreparationPhasesForPackage(secondaryPackage,
-                1 /* first preparation for this package*/);
-
-        // Install primary package
-        mTestSystemImpl.setPackageInfo(
-                createPackageInfo(primaryPackage, true /* enabled */ , true /* valid */,
-                    true /* installed */));
-        mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
-                WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID);
-
-        // Verify primary package used as provider, and secondary package killed
-        checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation for this package*/);
-        Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(secondaryPackage));
-    }
-
     @Test
     public void testRemovingSecondarySelectsPrimarySingleUser() {
         for (PackageRemovalType removalType : REMOVAL_TYPES) {
@@ -848,14 +744,6 @@
         checkRecoverAfterFailListingWebviewPackages(true);
     }
 
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    // If the flag is set, we don't automitally switch to second package unless it is chosen
-    // directly.
-    public void testRecoverFailedListingWebViewPackagesAddedPackage() {
-        checkRecoverAfterFailListingWebviewPackages(false);
-    }
-
     /**
      * Test that we can recover correctly from failing to list WebView packages.
      * settingsChange: whether to fail during changeProviderAndSetting or packageStateChanged
@@ -1114,31 +1002,6 @@
         }
     }
 
-    /**
-     * Ensure that the update service does not use an uninstalled package even if it is the only
-     * package available.
-     */
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    // If the flag is set, we return the package even if it is not installed.
-    public void testWithSingleUninstalledPackage() {
-        String testPackageName = "test.package.name";
-        WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-                new WebViewProviderInfo(testPackageName, "",
-                        true /*default available*/, false /* fallback */, null)};
-        setupWithPackages(webviewPackages);
-        mTestSystemImpl.setPackageInfo(createPackageInfo(testPackageName, true /* enabled */,
-                    true /* valid */, false /* installed */));
-
-        runWebViewBootPreparationOnMainSync();
-
-        Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged(
-                Matchers.anyObject());
-        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
-        assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
-        assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
-    }
-
     @Test
     public void testNonhiddenPackageUserOverHidden() {
         checkVisiblePackageUserOverNonVisible(false /* multiUser*/, PackageRemovalType.HIDE);
@@ -1374,95 +1237,6 @@
                 mWebViewUpdateServiceImpl.getCurrentWebViewPackage().versionName);
     }
 
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    public void testMultiProcessEnabledByDefault() {
-        testMultiProcessByDefault(true /* enabledByDefault */);
-    }
-
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    public void testMultiProcessDisabledByDefault() {
-        testMultiProcessByDefault(false /* enabledByDefault */);
-    }
-
-    private void testMultiProcessByDefault(boolean enabledByDefault) {
-        String primaryPackage = "primary";
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(
-                    primaryPackage, "", true /* default available */, false /* fallback */, null)};
-        setupWithPackagesAndMultiProcess(packages, enabledByDefault);
-        mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */,
-                    true /* valid */, true /* installed */, null /* signatures */,
-                    10 /* lastUpdateTime*/, false /* not hidden */, 1000 /* versionCode */,
-                    false /* isSystemApp */));
-
-        runWebViewBootPreparationOnMainSync();
-        checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */);
-
-        // Check it's off by default
-        assertEquals(enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
-
-        // Test toggling it
-        mWebViewUpdateServiceImpl.enableMultiProcess(!enabledByDefault);
-        assertEquals(!enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
-        mWebViewUpdateServiceImpl.enableMultiProcess(enabledByDefault);
-        assertEquals(enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
-    }
-
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    public void testMultiProcessEnabledByDefaultWithSettingsValue() {
-        testMultiProcessByDefaultWithSettingsValue(
-                true /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                true /* enabledByDefault */, -999999, true /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                true /* enabledByDefault */, 0, true /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                true /* enabledByDefault */, 999999, true /* expectEnabled */);
-    }
-
-    @Test
-    @RequiresFlagsDisabled("android.webkit.update_service_v2")
-    public void testMultiProcessDisabledByDefaultWithSettingsValue() {
-        testMultiProcessByDefaultWithSettingsValue(
-                false /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                false /* enabledByDefault */, 0, false /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                false /* enabledByDefault */, 999999, false /* expectEnabled */);
-        testMultiProcessByDefaultWithSettingsValue(
-                false /* enabledByDefault */, Integer.MAX_VALUE, true /* expectEnabled */);
-    }
-
-    /**
-     * Test the logic of the multiprocess setting depending on whether multiprocess is enabled by
-     * default, and what the setting is set to.
-     * @param enabledByDefault whether multiprocess is enabled by default.
-     * @param settingValue value of the multiprocess setting.
-     */
-    private void testMultiProcessByDefaultWithSettingsValue(
-            boolean enabledByDefault, int settingValue, boolean expectEnabled) {
-        String primaryPackage = "primary";
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(
-                    primaryPackage, "", true /* default available */, false /* fallback */, null)};
-        setupWithPackagesAndMultiProcess(packages, enabledByDefault);
-        mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */,
-                    true /* valid */, true /* installed */, null /* signatures */,
-                    10 /* lastUpdateTime*/, false /* not hidden */, 1000 /* versionCode */,
-                    false /* isSystemApp */));
-
-        runWebViewBootPreparationOnMainSync();
-        checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */);
-
-        mTestSystemImpl.setMultiProcessSetting(settingValue);
-
-        assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
-    }
-
-
     /**
      * Ensure that packages with a targetSdkVersion targeting the current platform are valid, and
      * that packages targeting an older version are not valid.
@@ -1507,7 +1281,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testDefaultWebViewPackageIsTheFirstAvailableByDefault() {
         String nonDefaultPackage = "nonDefaultPackage";
         String defaultPackage1 = "defaultPackage1";
@@ -1524,7 +1297,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testDefaultWebViewPackageEnabling() {
         String testPackage = "testDefault";
         WebViewProviderInfo[] packages =
@@ -1548,7 +1320,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testDefaultWebViewPackageInstallingDuringStartUp() {
         String testPackage = "testDefault";
         WebViewProviderInfo[] packages =
@@ -1572,7 +1343,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testDefaultWebViewPackageInstallingAfterStartUp() {
         String testPackage = "testDefault";
         WebViewProviderInfo[] packages =
@@ -1603,7 +1373,6 @@
      * the repair logic.
      */
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testAddingNewUserWithDefaultdPackageNotInstalled() {
         String testPackage = "testDefault";
         WebViewProviderInfo[] packages =
@@ -1651,7 +1420,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testDisabledDefaultPackageChosen() {
         PackageInfo disabledPackage =
                 createPackageInfo(
@@ -1664,7 +1432,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled("android.webkit.update_service_v2")
     public void testUninstalledDefaultPackageChosen() {
         PackageInfo uninstalledPackage =
                 createPackageInfo(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index b99ab05..ac021e1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -112,6 +112,7 @@
 import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT;
 import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
 import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -265,6 +266,7 @@
 import android.permission.PermissionManager;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.platform.test.rule.LimitDevicesRule;
@@ -485,6 +487,15 @@
 
     NotificationChannel mMinChannel = new NotificationChannel("min", "min", IMPORTANCE_MIN);
 
+    private final NotificationChannel mParentChannel =
+            new NotificationChannel(PARENT_CHANNEL_ID, "parentName", IMPORTANCE_DEFAULT);
+    private final NotificationChannel mConversationChannel =
+            new NotificationChannel(CONVERSATION_CHANNEL_ID, "conversationName", IMPORTANCE_DEFAULT);
+
+    private static final String PARENT_CHANNEL_ID = "parentChannelId";
+    private static final String CONVERSATION_CHANNEL_ID = "conversationChannelId";
+    private static final String CONVERSATION_ID = "conversationId";
+
     private static final int NOTIFICATION_LOCATION_UNKNOWN = 0;
 
     private static final String VALID_CONVO_SHORTCUT_ID = "shortcut";
@@ -4672,8 +4683,161 @@
         verify(mAmi).hasForegroundServiceNotification(anyString(), anyInt(), anyString());
     }
 
+    private void setUpChannelsForConversationChannelTest() throws RemoteException {
+        when(mPreferencesHelper.getNotificationChannel(
+                eq(mPkg), eq(mUid), eq(PARENT_CHANNEL_ID), eq(false)))
+                .thenReturn(mParentChannel);
+        when(mPreferencesHelper.getConversationNotificationChannel(
+                eq(mPkg), eq(mUid), eq(PARENT_CHANNEL_ID), eq(CONVERSATION_ID), eq(false), eq(false)))
+                .thenReturn(mConversationChannel);
+        when(mPackageManager.getPackageUid(mPkg, 0, mUserId)).thenReturn(mUid);
+    }
+
     @Test
-    public void testUpdateNotificationChannelFromPrivilegedListener_success() throws Exception {
+    @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public void createConversationChannelForPkgFromPrivilegedListener_cdm_success() throws Exception {
+        // Set up cdm
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(singletonList(mock(AssociationInfo.class)));
+
+        // Set up parent channel
+        setUpChannelsForConversationChannelTest();
+        final NotificationChannel parentChannelCopy = mParentChannel.copy();
+
+        NotificationChannel createdChannel =
+                mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener(
+                    null, mPkg, mUser, PARENT_CHANNEL_ID, CONVERSATION_ID);
+
+        // Verify that a channel is created and a copied channel is returned.
+        verify(mPreferencesHelper, times(1)).createNotificationChannel(
+                eq(mPkg), eq(mUid), any(), anyBoolean(), anyBoolean(),
+                eq(mUid), anyBoolean());
+        assertThat(createdChannel).isNotSameInstanceAs(mConversationChannel);
+        assertThat(createdChannel).isEqualTo(mConversationChannel);
+
+        // Verify that the channel creation is not directly use the parent channel.
+        verify(mPreferencesHelper, never()).createNotificationChannel(
+                anyString(), anyInt(), eq(mParentChannel), anyBoolean(), anyBoolean(),
+                anyInt(), anyBoolean());
+
+        // Verify that the content of parent channel is not changed.
+        assertThat(parentChannelCopy).isEqualTo(mParentChannel);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public void createConversationChannelForPkgFromPrivilegedListener_cdm_noAccess() throws Exception {
+        // Set up cdm without access
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(emptyList());
+
+        // Set up parent channel
+        setUpChannelsForConversationChannelTest();
+
+        try {
+            mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener(
+                null, mPkg, mUser, "parentId", "conversationId");
+            fail("listeners that don't have a companion device shouldn't be able to call this");
+        } catch (SecurityException e) {
+            // pass
+        }
+
+        verify(mPreferencesHelper, never()).createNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(), anyBoolean(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public void createConversationChannelForPkgFromPrivilegedListener_assistant_success() throws Exception {
+        // Set up assistant
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(emptyList());
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true);
+
+        // Set up parent channel
+        setUpChannelsForConversationChannelTest();
+        final NotificationChannel parentChannelCopy = mParentChannel.copy();
+
+        NotificationChannel createdChannel =
+                mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener(
+                    null, mPkg, mUser, PARENT_CHANNEL_ID, CONVERSATION_ID);
+
+        // Verify that a channel is created and a copied channel is returned.
+        verify(mPreferencesHelper, times(1)).createNotificationChannel(
+                eq(mPkg), eq(mUid), any(), anyBoolean(), anyBoolean(),
+                eq(mUid), anyBoolean());
+        assertThat(createdChannel).isNotSameInstanceAs(mConversationChannel);
+        assertThat(createdChannel).isEqualTo(mConversationChannel);
+
+        // Verify that the channel creation is not directly use the parent channel.
+        verify(mPreferencesHelper, never()).createNotificationChannel(
+                anyString(), anyInt(), eq(mParentChannel), anyBoolean(), anyBoolean(),
+                anyInt(), anyBoolean());
+
+        // Verify that the content of parent channel is not changed.
+        assertThat(parentChannelCopy).isEqualTo(mParentChannel);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public void createConversationChannelForPkgFromPrivilegedListener_assistant_noAccess() throws Exception {
+        // Set up assistant without access
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(emptyList());
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(false);
+
+        // Set up parent channel
+        setUpChannelsForConversationChannelTest();
+
+        try {
+            mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener(
+                null, mPkg, mUser, "parentId", "conversationId");
+            fail("listeners that don't have a companion device shouldn't be able to call this");
+        } catch (SecurityException e) {
+            // pass
+        }
+
+        verify(mPreferencesHelper, never()).createNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(), anyBoolean(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT)
+    public void createConversationChannelForPkgFromPrivilegedListener_badUser() throws Exception {
+        // Set up bad user
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(singletonList(mock(AssociationInfo.class)));
+        mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(mPkg, mPkg);
+        when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
+        when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+
+        // Set up parent channel
+        setUpChannelsForConversationChannelTest();
+
+        try {
+            mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener(
+                null, mPkg, mUser, "parentId", "conversationId");
+            fail("listener getting channels from a user they cannot see");
+        } catch (SecurityException e) {
+            // pass
+        }
+
+        verify(mPreferencesHelper, never()).createNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(), anyBoolean(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void updateNotificationChannelFromPrivilegedListener_cdm_success() throws Exception {
+
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
@@ -4693,7 +4857,7 @@
     }
 
     @Test
-    public void testUpdateNotificationChannelFromPrivilegedListener_noAccess() throws Exception {
+    public void updateNotificationChannelFromPrivilegedListener_cdm_noAccess() throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
                 .thenReturn(emptyList());
@@ -4715,7 +4879,51 @@
     }
 
     @Test
-    public void testUpdateNotificationChannelFromPrivilegedListener_badUser() throws Exception {
+    public void updateNotificationChannelFromPrivilegedListener_assistant_success() throws Exception {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(emptyList());
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true);
+        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
+                eq(mTestNotificationChannel.getId()), anyBoolean()))
+                .thenReturn(mTestNotificationChannel);
+
+        mBinderService.updateNotificationChannelFromPrivilegedListener(
+                null, mPkg, Process.myUserHandle(), mTestNotificationChannel);
+
+        verify(mPreferencesHelper, times(1)).updateNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(),  anyInt(), anyBoolean());
+
+        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
+                eq(Process.myUserHandle()), eq(mTestNotificationChannel),
+                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+    }
+
+    @Test
+    public void updateNotificationChannelFromPrivilegedListener_assistant_noAccess() throws Exception {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        when(mCompanionMgr.getAssociations(mPkg, mUserId))
+                .thenReturn(emptyList());
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(false);
+
+        try {
+            mBinderService.updateNotificationChannelFromPrivilegedListener(
+                    null, mPkg, Process.myUserHandle(), mTestNotificationChannel);
+            fail("listeners that don't have a companion device shouldn't be able to call this");
+        } catch (SecurityException e) {
+            // pass
+        }
+
+        verify(mPreferencesHelper, never()).updateNotificationChannel(
+                anyString(), anyInt(), any(), anyBoolean(),  anyInt(), anyBoolean());
+
+        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
+                eq(Process.myUserHandle()), eq(mTestNotificationChannel),
+                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+    }
+
+    @Test
+    public void updateNotificationChannelFromPrivilegedListener_badUser() throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
@@ -4741,7 +4949,7 @@
     }
 
     @Test
-    public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission()
+    public void updateNotificationChannelFromPrivilegedListener_noSoundUriPermission()
             throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
@@ -4773,7 +4981,7 @@
     }
 
     @Test
-    public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound()
+    public void updateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound()
             throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
@@ -4805,7 +5013,7 @@
 
     @Test
     public void
-        testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
+        updateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
             throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mCompanionMgr.getAssociations(mPkg, mUserId))
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index bdf146f..3236f95 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -528,8 +528,8 @@
         String xml = "<zen version=\"12\">\n"
                 + "<allow calls=\"true\" repeatCallers=\"true\" messages=\"true\""
                 + " reminders=\"false\" events=\"false\" callsFrom=\"2\" messagesFrom=\"2\""
-                + " alarms=\"true\" media=\"true\" system=\"false\" convos=\"true\" convosFrom=\"2\""
-                + " priorityChannelsAllowed=\"true\" />\n"
+                + " alarms=\"true\" media=\"true\" system=\"false\" convos=\"true\""
+                + " convosFrom=\"2\" priorityChannelsAllowed=\"true\" />\n"
                 + "<disallow visualEffects=\"157\" />\n"
                 + "<manual enabled=\"true\" zen=\"1\" creationTime=\"0\" modified=\"false\" />\n"
                 + "<state areChannelsBypassingDnd=\"true\" />\n"
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 6792377..0019b3e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -6249,6 +6249,50 @@
     }
 
     @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void applyGlobalZenModeAsImplicitZenRule_again_refreshesRuleName() {
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+        // "Break" the rule name to check that applying again restores it.
+        mZenModeHelper.mConfig.automaticRules.valueAt(0).name = "BOOM!";
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void applyGlobalZenModeAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+        String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName());
+
+        // User chooses a new name.
+        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                new AutomaticZenRule.Builder(azr).setName("User chose this").build(),
+                ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
+
+        // App triggers the rule again.
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("User chose this");
+    }
+
+    @Test
     @DisableFlags(FLAG_MODES_API)
     public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -6399,6 +6443,50 @@
     }
 
     @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void applyGlobalPolicyAsImplicitZenRule_again_refreshesRuleName() {
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0));
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+        // "Break" the rule name to check that updating it again restores it.
+        mZenModeHelper.mConfig.automaticRules.valueAt(0).name = "BOOM!";
+
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void applyGlobalPolicyAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() {
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0));
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")");
+        String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName());
+
+        // User chooses a new name.
+        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                new AutomaticZenRule.Builder(azr).setName("User chose this").build(),
+                ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
+
+        // App updates the implicit rule again.
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT,
+                mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name)
+                .isEqualTo("User chose this");
+    }
+
+    @Test
     @DisableFlags(FLAG_MODES_API)
     public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
         mZenModeHelper.mConfig.automaticRules.clear();
@@ -7247,9 +7335,10 @@
         rule.zenMode = zenMode;
         rule.zenPolicy = policy;
         rule.pkg = ownerPkg;
-        rule.name = CUSTOM_APP_LABEL;
-        if (!Flags.modesUi()) {
-            rule.iconResName = ICON_RES_NAME;
+        if (Flags.modesUi()) {
+            rule.name = mContext.getString(R.string.zen_mode_implicit_name, CUSTOM_APP_LABEL);
+        } else {
+            rule.name = CUSTOM_APP_LABEL;
         }
         rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description,
                 CUSTOM_APP_LABEL);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 3742249..b8cfa7c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -143,6 +143,10 @@
         doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
     }
 
+    void setDisplayIgnoreActivitySizeRestrictions(boolean enabled) {
+        doReturn(enabled).when(mDisplayContent).isDisplayIgnoreActivitySizeRestrictions();
+    }
+
     void configureTaskBounds(@NonNull Rect taskBounds) {
         doReturn(taskBounds).when(mTaskStack.top()).getBounds();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
index b839113..14ef913 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -17,9 +17,12 @@
 package com.android.server.wm;
 
 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.view.Surface.ROTATION_90;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
 
@@ -29,10 +32,13 @@
 import static org.junit.Assert.assertNotEquals;
 
 import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
 
+import com.android.window.flags.Flags;
+
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
@@ -288,6 +294,93 @@
         });
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testHasFullscreenOverride_displayIgnoreActivitySizeRestrictionsTrue() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.setDisplayIgnoreActivitySizeRestrictions(true);
+                a.createActivityWithComponent();
+            });
+
+            robot.checkHasFullscreenOverride(true);
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testHasFullscreenOverride_displayIgnoreActivitySizeRestrictionsFalse() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.setDisplayIgnoreActivitySizeRestrictions(false);
+                a.createActivityWithComponent();
+            });
+
+            robot.checkHasFullscreenOverride(false);
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testPropFalse_displayIgnoreActivitySizeRestrictionsTrue_notOverridden() {
+        runTestScenario((robot) -> {
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+            robot.applyOnActivity((a) -> {
+                a.setDisplayIgnoreActivitySizeRestrictions(true);
+                a.createActivityWithComponent();
+            });
+
+            robot.checkHasFullscreenOverride(false);
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testPropTrue_displayIgnoreActivitySizeRestrictionsFalse_notOverridden() {
+        runTestScenario((robot) -> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+            robot.applyOnActivity((a) -> {
+                a.setDisplayIgnoreActivitySizeRestrictions(false);
+                a.createActivityWithComponent();
+            });
+
+            robot.checkHasFullscreenOverride(false);
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testNotInSizeCompatMode_displayIgnoreActivitySizeRestrictionsTrue() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponent();
+                a.setDisplayIgnoreActivitySizeRestrictions(true);
+                a.configureTopActivity(/* minAspect */ -1f, /* maxAspect */-1f,
+                        SCREEN_ORIENTATION_LANDSCAPE, true);
+                a.rotateDisplayForTopActivity(ROTATION_90);
+
+                a.checkTopActivityInSizeCompatMode(false);
+            });
+        });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API)
+    public void testInSizeCompatMode_displayIgnoreActivitySizeRestrictionsFalse() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponent();
+                a.setIgnoreOrientationRequest(true);
+                a.setDisplayIgnoreActivitySizeRestrictions(false);
+                a.configureTopActivity(/* minAspect */ -1f, /* maxAspect */-1f,
+                        SCREEN_ORIENTATION_LANDSCAPE, true);
+                a.rotateDisplayForTopActivity(ROTATION_90);
+
+                a.checkTopActivityInSizeCompatMode(true);
+            });
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -366,6 +459,11 @@
             }
         }
 
+        void checkHasFullscreenOverride(boolean expected) {
+            assertEquals(expected,
+                    getTopActivityAppCompatAspectRatioOverrides().hasFullscreenOverride());
+        }
+
         private AppCompatAspectRatioOverrides getTopActivityAppCompatAspectRatioOverrides() {
             return activity().top().mAppCompatController.getAppCompatAspectRatioOverrides();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index 7bc9f30..db3ce0b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -194,6 +194,7 @@
         mService.mTaskSupervisor = mSupervisor;
         mService.mContext = mContext;
         setViaReflection(mService, "mActiveUids", mActiveUids);
+        setViaReflection(mService, "mGlobalLock", new WindowManagerGlobalLock());
         Mockito.when(mService.getPackageManagerInternalLocked()).thenReturn(
                 mPackageManagerInternal);
         mService.mRootWindowContainer = mRootWindowContainer;
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index 4ccbc32..66d75f7 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -907,29 +907,6 @@
     }
 
     /**
-     * Ensure the caller (or self, if not processing an IPC) has
-     * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} or
-     * {@link android.Manifest.permission#READ_PHONE_NUMBERS}.
-     *
-     * @throws SecurityException if the caller does not have the required permission/privileges
-     */
-    @RequiresPermission(anyOf = {
-            android.Manifest.permission.READ_PHONE_NUMBERS,
-            android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
-    })
-    public static boolean checkCallingOrSelfReadPrivilegedPhoneStatePermissionOrReadPhoneNumber(
-            Context context, int subId, String callingPackage, @Nullable String callingFeatureId,
-            String message) {
-        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
-            return false;
-        }
-        return (context.checkCallingOrSelfPermission(
-                Manifest.permission.READ_PRIVILEGED_PHONE_STATE) == PERMISSION_GRANTED
-                || checkCallingOrSelfReadPhoneNumber(context, subId, callingPackage,
-                callingFeatureId, message));
-    }
-
-    /**
      * @return true if the specified {@code uid} is for a system or phone process, no matter if runs
      * as system user or not.
      */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index fad59f8b..6f2c862 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -133,6 +133,7 @@
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.RILConstants;
 import com.android.internal.telephony.flags.Flags;
+import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.telephony.Rlog;
 
@@ -6282,14 +6283,17 @@
      * The contents of the file is a <b>Ip Multimedia Service Private User Identity</b> of the user
      * as defined in the section 4.2.2 of 3GPP TS 131 103.
      *
-     * @return IMPI (IMS private user identity) of type string.
+     * @return IMPI (IMS private user identity) of type string or null if the IMPI isn't present
+     *         on the ISIM.
      * @throws IllegalStateException in case the ISIM has’t been loaded
      * @throws SecurityException if the caller does not have the required permission/privileges
      * @hide
      */
-    @NonNull
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER)
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+    @Nullable
     public String getImsPrivateUserIdentity() {
         try {
             IPhoneSubInfo info = getSubscriberInfoService();
@@ -6370,6 +6374,9 @@
      * The contents of the file are <b>Ip Multimedia Service Public User Identities</b> of the user
      * as defined in the section 4.2.4 of 3GPP TS 131 103. It contains one or more records.
      *
+     * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission or carrier
+     * privileges.
+     *
      * @return List of public user identities of type android.net.Uri or empty list  if
      *         EF_IMPU is not available.
      * @throws IllegalStateException in case the ISIM hasn’t been loaded
@@ -6378,18 +6385,18 @@
      *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
-    @NonNull
-    @RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_NUMBERS,
-            android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
+    @SystemApi
+    @RequiresPermission(value = Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional = true)
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+    @NonNull
     public List<Uri> getImsPublicUserIdentities() {
         try {
             IPhoneSubInfo info = getSubscriberInfoService();
             if (info == null) {
                 throw new RuntimeException("IMPU error: Subscriber Info is null");
             }
-            return info.getImsPublicUserIdentities(getSubId(), getOpPackageName(),
-                    getAttributionTag());
+            return info.getImsPublicUserIdentities(getSubId(), getOpPackageName());
         } catch (IllegalArgumentException | NullPointerException ex) {
             Rlog.e(TAG, "getImsPublicUserIdentities Exception = " + ex);
         } catch (RemoteException ex) {
@@ -8684,7 +8691,10 @@
      * @return an array of PCSCF strings with one PCSCF per string, or null if
      *         not present or not loaded
      * @hide
+     * @deprecated use {@link #getImsPcscfAddresses()} instead.
      */
+    @Deprecated
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
     @UnsupportedAppUsage
     public String[] getIsimPcscf() {
         try {
@@ -8701,6 +8711,40 @@
         }
     }
 
+    /**
+     * Returns the IMS Proxy Call Session Control Function(P-CSCF) that were loaded from the ISIM.
+     *
+     * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission or carrier
+     * privileges.
+     *
+     * @return List of P-CSCF address strings or empty list if not available.
+     * @throws IllegalStateException in case the ISIM hasn’t been loaded
+     * @throws SecurityException if the caller does not have the required permission/privilege
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
+    @SystemApi
+    @RequiresPermission(value = Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional = true)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+    @NonNull
+    public List<String> getImsPcscfAddresses() {
+        try {
+            IPhoneSubInfo info = getSubscriberInfoService();
+            if (info == null) {
+                throw new RuntimeException("P-CSCF error: Subscriber Info is null");
+            }
+            return info.getImsPcscfAddresses(getSubId(), getOpPackageName());
+        } catch (IllegalArgumentException | NullPointerException ex) {
+            Rlog.e(TAG, "getImsPcscfAddresses Exception = " + ex);
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "getImsPcscfAddresses Exception = " + ex);
+            ex.rethrowAsRuntimeException();
+        }
+        return Collections.EMPTY_LIST;
+    }
+
     /** UICC application type is unknown or not specified */
     public static final int APPTYPE_UNKNOWN = PhoneConstants.APPTYPE_UNKNOWN;
     /** UICC application type is SIM */
@@ -8934,8 +8978,10 @@
      * @throws UnsupportedOperationException If the device does not have
      *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
+     * @deprecated Use {@link #getSimServiceTable(int, Executor, OutcomeReceiver)} instead.
      */
-
+    @Deprecated
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
     @Nullable
     @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
@@ -8963,6 +9009,55 @@
     }
 
     /**
+     * Fetches the sim service table from the EFUST/EFIST based on the application type
+     * {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}.
+     * The USIM service table EF is described in as per Section 4.2.8 of 3GPP TS 31.102.
+     * The ISIM service table EF is described in as per Section 4.2.7 of 3GPP TS 31.103.
+     *
+     * @param appType of type int of either {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}.
+     * @param executor executor to run the callback on.
+     * @param callback callback object to which the result will be delivered.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD)
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+    public void getSimServiceTable(@UiccAppType int appType,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<byte[], Exception> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        IPhoneSubInfo info = getSubscriberInfoService();
+        if (info == null) {
+            executor.execute(() -> callback.onError(
+                    new RuntimeException("getSimServiceTable: Subscriber Info is null")));
+            return;
+        }
+
+        try {
+            String serviceTable;
+            if (appType == APPTYPE_ISIM) {
+                serviceTable  = info.getIsimIst(getSubId());
+            } else if ((appType == APPTYPE_USIM)) {
+                serviceTable = info.getSimServiceTable(getSubId(), APPTYPE_USIM);
+            } else {
+                serviceTable = null;
+            }
+
+            if (serviceTable == null) {
+                executor.execute(() -> callback.onResult(new byte[0]));
+            } else {
+                byte[] simServiceTable = IccUtils.hexStringToBytes(serviceTable);
+                executor.execute(() -> callback.onResult(simServiceTable));
+            }
+        } catch (Exception ex) {
+            executor.execute(() -> callback.onError(ex));
+        }
+    }
+
+    /**
      * Resets the {@link android.telephony.ims.ImsService} associated with the specified sim slot.
      * Used by diagnostic apps to force the IMS stack to be disabled and re-enabled in an effort to
      * recover from scenarios where the {@link android.telephony.ims.ImsService} gets in to a bad
diff --git a/telephony/java/android/telephony/ims/ImsCallSessionListener.java b/telephony/java/android/telephony/ims/ImsCallSessionListener.java
index 5946535..00d4bd0 100644
--- a/telephony/java/android/telephony/ims/ImsCallSessionListener.java
+++ b/telephony/java/android/telephony/ims/ImsCallSessionListener.java
@@ -16,6 +16,7 @@
 
 package android.telephony.ims;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -31,6 +32,7 @@
 import android.util.Log;
 
 import com.android.ims.internal.IImsCallSession;
+import com.android.internal.telephony.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.Objects;
@@ -802,8 +804,8 @@
 
     /**
      * Notifies the result of transfer request.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
     public void callSessionTransferred() {
         try {
             mListener.callSessionTransferred();
@@ -813,13 +815,13 @@
     }
 
     /**
-     * Notifies the result of transfer request.
+     * Notifies the result of the transfer request failure.
      *
      * @param reasonInfo {@link ImsReasonInfo} containing a reason for the
      * session transfer failure
-     * @hide
      */
-    public void callSessionTransferFailed(ImsReasonInfo reasonInfo) {
+    @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    public void callSessionTransferFailed(@NonNull ImsReasonInfo reasonInfo) {
         try {
             mListener.callSessionTransferFailed(reasonInfo);
         } catch (RemoteException e) {
@@ -839,8 +841,8 @@
      * @param bitsPerSecond This value is the bitrate requested by the other party UE through
      *        RTP CMR, RTCPAPP or TMMBR, and ImsStack converts this value to the MAC bitrate
      *        (defined in TS36.321, range: 0 ~ 8000 kbit/s).
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
     public final void callSessionSendAnbrQuery(@MediaStreamType int mediaType,
                 @MediaStreamDirection int direction, @IntRange(from = 0) int bitsPerSecond) {
         Log.d(TAG, "callSessionSendAnbrQuery in imscallsessonListener");
diff --git a/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java b/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java
index 88d9aae..81cddb7 100644
--- a/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java
+++ b/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java
@@ -16,12 +16,16 @@
 
 package android.telephony.ims.feature;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.SparseArray;
 
+import com.android.internal.telephony.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -30,6 +34,8 @@
  *
  * @hide
  */
+@FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+@SystemApi
 public final class ConnectionFailureInfo implements Parcelable {
 
     /** @hide */
@@ -67,7 +73,7 @@
     public static final int REASON_RRC_TIMEOUT = 6;
     /** Device currently not in service */
     public static final int REASON_NO_SERVICE = 7;
-    /** The PDN is no more active */
+    /** The PDN is no longer active */
     public static final int REASON_PDN_NOT_AVAILABLE = 8;
     /** Radio resource is busy with another subscription */
     public static final int REASON_RF_BUSY = 9;
@@ -135,6 +141,8 @@
 
     /**
      * @return the cause code from the network or modem specific to the failure.
+     *         See 3GPP TS 24.401 Annex A (Cause values for EPS mobility management) and
+     *         3GPP TS 24.501 Annex A (Cause values for 5GS mobility management).
      */
     public int getCauseCode() {
         return mCauseCode;
diff --git a/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java b/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java
index 245ee15..0029d49 100644
--- a/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java
+++ b/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java
@@ -16,20 +16,26 @@
 
 package android.telephony.ims.feature;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import com.android.internal.telephony.flags.Flags;
 
 /**
  * A callback class used to receive the result of {@link MmTelFeature#startImsTrafficSession}.
  * @hide
  */
+@FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+@SystemApi
 public interface ImsTrafficSessionCallback {
 
     /** The modem is ready to process the IMS traffic. */
     void onReady();
 
     /**
-     * Notifies that any IMS traffic is not sent to network due to any failure
-     * on cellular networks. IMS service shall call {@link MmTelFeature#stopImsTrafficSession()}
+     * Notifies that any IMS traffic can not be sent to the network due to the provided cellular
+     * network failure. IMS service shall call {@link MmTelFeature#stopImsTrafficSession()}
      * when receiving this callback.
      *
      * @param info The information of the failure.
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index 3f02ae9..c6b11d7 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -16,6 +16,8 @@
 
 package android.telephony.ims.feature;
 
+import static com.android.internal.telephony.flags.Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -964,6 +966,8 @@
      *
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1;
 
     /**
@@ -976,6 +980,8 @@
      *
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2;
 
     /** @hide */
@@ -1003,36 +1009,50 @@
      * Emergency call
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_EMERGENCY = 0;
     /**
      * Emergency SMS
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_EMERGENCY_SMS = 1;
     /**
      * Voice call
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_VOICE = 2;
     /**
      * Video call
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_VIDEO = 3;
     /**
      * SMS over IMS
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_SMS = 4;
     /**
      * IMS registration and subscription for reg event package (signaling)
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_REGISTRATION = 5;
     /**
      * Ut/XCAP (XML Configuration Access Protocol)
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_TYPE_UT_XCAP = 6;
 
     /** @hide */
@@ -1046,11 +1066,15 @@
      * Indicates that the traffic is an incoming traffic.
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_DIRECTION_INCOMING = 0;
     /**
      * Indicates that the traffic is an outgoing traffic.
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public static final int IMS_TRAFFIC_DIRECTION_OUTGOING = 1;
 
     private IImsMmTelListener mListener;
@@ -1291,9 +1315,11 @@
     /**
      * Triggers the EPS fallback procedure.
      *
-     * @param reason specifies the reason that causes EPS fallback.
+     * @param reason specifies the reason that EPS fallback was triggered.
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public final void triggerEpsFallback(@EpsFallbackReason int reason) {
         IImsMmTelListener listener = getListener();
         if (listener == null) {
@@ -1344,6 +1370,8 @@
      *
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public final void startImsTrafficSession(@ImsTrafficType int trafficType,
             @AccessNetworkConstants.RadioAccessNetworkType int accessNetworkType,
             @ImsTrafficDirection int trafficDirection,
@@ -1387,6 +1415,8 @@
      *
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public final void modifyImsTrafficSession(
             @AccessNetworkConstants.RadioAccessNetworkType int accessNetworkType,
             @NonNull ImsTrafficSessionCallback callback) {
@@ -1417,6 +1447,8 @@
      *
      * @hide
      */
+    @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE)
+    @SystemApi
     public final void stopImsTrafficSession(@NonNull ImsTrafficSessionCallback callback) {
         IImsMmTelListener listener = getListener();
         if (listener == null) {
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index c78dbe7..e332d0f 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -68,7 +68,6 @@
  */
 @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SATELLITE)
 @SystemApi
-@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
 public final class SatelliteManager {
     private static final String TAG = "SatelliteManager";
 
diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
index b4d93fd..974cc14 100644
--- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
+++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
@@ -208,10 +208,9 @@
     /**
      * Fetches the ISIM public user identities (EF_IMPU) from UICC based on subId
      */
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" +
-    "anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})")
-     List<Uri> getImsPublicUserIdentities(int subId, String callingPackage,
-                                           String callingFeatureId);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
+     List<Uri> getImsPublicUserIdentities(int subId, String callingPackage);
 
     /**
      * Returns the IMS Service Table (IST) that was loaded from the ISIM.
@@ -227,6 +226,20 @@
     String[] getIsimPcscf(int subId);
 
     /**
+      * Fetches IMS Proxy Call Session Control Function(P-CSCF) based on the subscription.
+      *
+      * @param subId subscriptionId
+      * @param callingPackage package name of the caller
+      * @return List of IMS Proxy Call Session Control Function strings.
+      * @throws IllegalArgumentException if the subscriptionId is not valid
+      * @throws IllegalStateException in case the ISIM hasn’t been loaded.
+      * @throws SecurityException if the caller does not have the required permission
+      */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
+    List<String> getImsPcscfAddresses(int subId, String callingPackage);
+
+    /**
      * Returns the response of the SIM application on the UICC to authentication
      * challenge/response algorithm. The data string and challenge response are
      * Base64 encoded Strings.
diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java
index 6bf7ff5..3247d1f 100644
--- a/test-mock/src/android/test/mock/MockContext.java
+++ b/test-mock/src/android/test/mock/MockContext.java
@@ -53,6 +53,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -607,6 +608,12 @@
         throw new UnsupportedOperationException();
     }
 
+    /** @hide */
+    @Override
+    public List<IntentFilter> getRegisteredIntentFilters(BroadcastReceiver receiver) {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public ComponentName startService(Intent service) {
         throw new UnsupportedOperationException();
diff --git a/tests/Input/src/com/android/server/input/InputShellCommandTest.java b/tests/Input/src/com/android/server/input/InputShellCommandTest.java
index 11f4633..a236244 100644
--- a/tests/Input/src/com/android/server/input/InputShellCommandTest.java
+++ b/tests/Input/src/com/android/server/input/InputShellCommandTest.java
@@ -133,6 +133,21 @@
         assertThat(mInputEventInjector.mInjectedEvents).isEmpty();
     }
 
+    @Test
+    public void testSwipeCommandEventFrequency() {
+        int[] durations = {100, 300, 500};
+        for (int durationMillis: durations) {
+            mInputEventInjector.mInjectedEvents.clear();
+            runCommand(String.format("swipe 200 800 200 200 %d", durationMillis));
+
+            // Add 2 events for ACTION_DOWN and ACTION_UP.
+            final int maxEventNum =
+                    (int) Math.ceil(InputShellCommand.SWIPE_EVENT_HZ_DEFAULT
+                            * (float) durationMillis / 1000) + 2;
+            assertThat(mInputEventInjector.mInjectedEvents.size()).isAtMost(maxEventNum);
+        }
+    }
+
     private InputEvent getSingleInjectedInputEvent() {
         assertThat(mInputEventInjector.mInjectedEvents).hasSize(1);
         return mInputEventInjector.mInjectedEvents.get(0);
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 19dea0c..a7e2214 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -884,6 +884,86 @@
     }
 
     @Test
+    @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
+    fun testSnapLeftFreeformTask() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "ALT + [ -> Resizes a task to fit the left half of the screen",
+                intArrayOf(
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_LEFT_BRACKET
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
+                intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET),
+                KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
+    fun testSnapRightFreeformTask() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "ALT + ] -> Resizes a task to fit the right half of the screen",
+                intArrayOf(
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_RIGHT_BRACKET
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
+                intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET),
+                KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
+    fun testMaximizeFreeformTask() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "ALT + '=' -> Maximizes a task to fit the screen",
+                intArrayOf(
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_EQUALS
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+                intArrayOf(KeyEvent.KEYCODE_EQUALS),
+                KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
+    @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
+    fun testRestoreFreeformTask() {
+        val keyGestureController = KeyGestureController(context, testLooper.looper)
+        testKeyGestureInternal(
+            keyGestureController,
+            TestData(
+                "ALT + '-' -> Restores a task size to its previous bounds",
+                intArrayOf(
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_MINUS
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
+                intArrayOf(KeyEvent.KEYCODE_MINUS),
+                KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            )
+        )
+    }
+
+    @Test
     fun testCapsLockPressNotified() {
         val keyGestureController = KeyGestureController(context, testLooper.looper)
         val listener = KeyGestureEventListener()
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 7e0bbc4..3828a71 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -70,6 +70,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.Uri;
+import android.net.vcn.Flags;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
 import android.net.vcn.VcnConfig;
@@ -84,6 +85,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -102,6 +104,7 @@
 import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -119,6 +122,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VcnManagementServiceTest {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final String CONTEXT_ATTRIBUTION_TAG = "VCN";
     private static final String TEST_PACKAGE_NAME =
             VcnManagementServiceTest.class.getPackage().getName();
@@ -288,6 +293,8 @@
         doReturn(Collections.singleton(TRANSPORT_WIFI))
                 .when(mMockDeps)
                 .getRestrictedTransports(any(), any(), any());
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_FIX_CONFIG_GARBAGE_COLLECTION);
     }
 
 
@@ -438,6 +445,14 @@
             return subIds;
         }).when(snapshot).getAllSubIdsInGroup(any());
 
+        doAnswer(invocation -> {
+            final Set<ParcelUuid> subGroups = new ArraySet<>();
+            for (Entry<Integer, ParcelUuid> entry : subIdToGroupMap.entrySet()) {
+                subGroups.add(entry.getValue());
+            }
+            return subGroups;
+        }).when(snapshot).getAllSubscriptionGroups();
+
         return snapshot;
     }
 
@@ -1483,6 +1498,28 @@
     }
 
     @Test
+    public void testGarbageCollectionKeepConfigUntilNewSnapshot() throws Exception {
+        setupActiveSubscription(TEST_UUID_2);
+        startAndGetVcnInstance(TEST_UUID_2);
+
+        // Report loss of subscription from mSubMgr
+        doReturn(Collections.emptyList()).when(mSubMgr).getSubscriptionsInGroup(any());
+        triggerSubscriptionTrackerCbAndGetSnapshot(
+                TEST_UUID_2,
+                Collections.singleton(TEST_UUID_2),
+                Collections.singletonMap(TEST_SUBSCRIPTION_ID, TEST_UUID_2));
+
+        assertTrue(mVcnMgmtSvc.getConfigs().containsKey(TEST_UUID_2));
+
+        // Report loss of subscription from snapshot
+        triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet());
+
+        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
+        mTestLooper.dispatchAll();
+        assertFalse(mVcnMgmtSvc.getConfigs().containsKey(TEST_UUID_2));
+    }
+
+    @Test
     public void testVcnCarrierConfigChangeUpdatesPolicyListener() throws Exception {
         setupActiveSubscription(TEST_UUID_2);
 
diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index 514651e..449d93d 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -213,15 +213,28 @@
 
     bool match = false;
     for (Flag& flag : flags_) {
-      if (arg == flag.name) {
+      // Allow both "--arg value" and "--arg=value" syntax.
+      if (arg.starts_with(flag.name) &&
+          (arg.size() == flag.name.size() || (flag.num_args > 0 && arg[flag.name.size()] == '='))) {
         if (flag.num_args > 0) {
-          i++;
-          if (i >= args.size()) {
-            *out_error << flag.name << " missing argument.\n\n";
-            Usage(out_error);
-            return false;
+          if (arg.size() == flag.name.size()) {
+            i++;
+            if (i >= args.size()) {
+              *out_error << flag.name << " missing argument.\n\n";
+              Usage(out_error);
+              return 1;
+            }
+            arg = args[i];
+          } else {
+            arg.remove_prefix(flag.name.size() + 1);
+            // Disallow empty arguments after '='.
+            if (arg.empty()) {
+              *out_error << flag.name << " has empty argument.\n\n";
+              Usage(out_error);
+              return 1;
+            }
           }
-          flag.action(args[i]);
+          flag.action(arg);
         } else {
           flag.action({});
         }
diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp
index 7aa1aa01..20d87e0 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -19,6 +19,7 @@
 #include "test/Test.h"
 
 using ::testing::Eq;
+using namespace std::literals;
 
 namespace aapt {
 
@@ -94,4 +95,27 @@
 }
 #endif
 
+TEST(CommandTest, OptionsWithValues) {
+  TestCommand command;
+  std::string flag;
+  command.AddRequiredFlag("--flag", "", &flag);
+
+  ASSERT_EQ(0, command.Execute({"--flag"s, "1"s}, &std::cerr));
+  EXPECT_STREQ("1", flag.c_str());
+
+  ASSERT_EQ(0, command.Execute({"--flag=1"s}, &std::cerr));
+  EXPECT_STREQ("1", flag.c_str());
+
+  ASSERT_EQ(0, command.Execute({"--flag"s, "=2"s}, &std::cerr));
+  EXPECT_STREQ("=2", flag.c_str());
+
+  ASSERT_EQ(0, command.Execute({"--flag"s, "--flag"s}, &std::cerr));
+  EXPECT_STREQ("--flag", flag.c_str());
+
+  EXPECT_NE(0, command.Execute({"--flag"s}, &std::cerr));
+  EXPECT_NE(0, command.Execute({"--flag="s}, &std::cerr));
+  EXPECT_NE(0, command.Execute({"--flag1=2"s}, &std::cerr));
+  EXPECT_NE(0, command.Execute({"--flag1"s, "2"s}, &std::cerr));
+}
+
 }  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index 52372fa..a5e18d35 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -605,8 +605,9 @@
     }
 
     // Write the crunched PNG.
-    if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out, {},
-                           &source_diag, context->IsVerbose())) {
+    if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out,
+                           {.compression_level = options.png_compression_level_int}, &source_diag,
+                           context->IsVerbose())) {
       return false;
     }
 
@@ -924,6 +925,19 @@
     }
   }
 
+  if (!options_.png_compression_level) {
+    options_.png_compression_level_int = 9;
+  } else {
+    if (options_.png_compression_level->size() != 1 ||
+        options_.png_compression_level->front() < '0' ||
+        options_.png_compression_level->front() > '9') {
+      context.GetDiagnostics()->Error(
+          android::DiagMessage() << "PNG compression level should be a number in [0..9] range");
+      return 1;
+    }
+    options_.png_compression_level_int = options_.png_compression_level->front() - '0';
+  }
+
   return Compile(&context, file_collection.get(), archive_writer.get(), options_);
 }
 
diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h
index 70c8791..e244546 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -47,6 +47,8 @@
   bool verbose = false;
   std::optional<std::string> product_;
   FeatureFlagValues feature_flag_values;
+  std::optional<std::string> png_compression_level;
+  int png_compression_level_int = 9;
 };
 
 /** Parses flags and compiles resources to be used in linking.  */
@@ -65,6 +67,9 @@
     AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales "
         "(en-XA and ar-XB)", &options_.pseudolocalize);
     AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch);
+    AddOptionalFlag("--png-compression-level",
+                    "Set the zlib compression level for crunched PNG images, [0-9], 9 by default.",
+                    &options_.png_compression_level);
     AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings",
         &options_.legacy_mode);
     AddOptionalSwitch("--preserve-visibility-of-styleables",